Add OAuth login support and streamline authentication flows
- Introduced `oauthLogin` method in `electron/preload.ts` and backend IPC handlers for OAuth via `BrowserWindow`. - Replaced web-based OAuth redirection with Electron-specific implementation for Google, Facebook, and Apple. - Refactored `SocialForm.tsx` to handle OAuth login success and token management via Electron. - Updated `User`, `QuillSense`, and context methods to include `quill-trial` subscriptions and extended login logic. - Cleaned up code, removed unused imports, and improved error handling for authentication scenarios.
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
import {faApple, faFacebookF, faGoogle} from "@fortawesome/free-brands-svg-icons";
|
import {faApple, faFacebookF, faGoogle} from "@fortawesome/free-brands-svg-icons";
|
||||||
import React, {useContext, useEffect} from "react";
|
import React, {useContext, useEffect} from "react";
|
||||||
import Link from "next/link";
|
|
||||||
import System from "@/lib/models/System";
|
import System from "@/lib/models/System";
|
||||||
import {AlertContext} from "@/context/AlertContext";
|
import {AlertContext} from "@/context/AlertContext";
|
||||||
import {configs} from "@/lib/configs";
|
import {configs} from "@/lib/configs";
|
||||||
@@ -14,8 +13,12 @@ export default function SocialForm() {
|
|||||||
const {setSession} = useContext<SessionContextProps>(SessionContext)
|
const {setSession} = useContext<SessionContextProps>(SessionContext)
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const {lang} = useContext<LangContextProps>(LangContext)
|
const {lang} = useContext<LangContextProps>(LangContext)
|
||||||
|
const isElectron = typeof window !== 'undefined' && !!window.electron;
|
||||||
|
|
||||||
useEffect((): void => {
|
useEffect((): void => {
|
||||||
|
// Skip URL parsing in Electron (OAuth is handled via BrowserWindow)
|
||||||
|
if (isElectron) return;
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const provider: string | null = params.get('provider');
|
const provider: string | null = params.get('provider');
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
@@ -47,6 +50,16 @@ export default function SocialForm() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
async function handleLoginSuccess(token: string): Promise<void> {
|
||||||
|
if (window.electron) {
|
||||||
|
await window.electron.setToken(token);
|
||||||
|
window.electron.loginSuccess(token);
|
||||||
|
} else {
|
||||||
|
System.setCookie('token', token, 30);
|
||||||
|
setSession({isConnected: true, user: null, accessToken: token});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleFacebookLogin(code: string, state: string): Promise<void> {
|
async function handleFacebookLogin(code: string, state: string): Promise<void> {
|
||||||
if (code && state) {
|
if (code && state) {
|
||||||
const response: string = await System.postToServer<string>(`auth/facebook`, {
|
const response: string = await System.postToServer<string>(`auth/facebook`, {
|
||||||
@@ -57,13 +70,7 @@ export default function SocialForm() {
|
|||||||
errorMessage(t('socialForm.error.connection'));
|
errorMessage(t('socialForm.error.connection'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
System.setCookie('token', response, 30);
|
await handleLoginSuccess(response);
|
||||||
const token: string | null = System.getCookie('token');
|
|
||||||
if (!token) {
|
|
||||||
errorMessage(t('socialForm.error.connection'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSession({isConnected: true, user: null, accessToken: token});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,14 +81,9 @@ export default function SocialForm() {
|
|||||||
}, lang);
|
}, lang);
|
||||||
if (!response) {
|
if (!response) {
|
||||||
errorMessage(t('socialForm.error.connection'));
|
errorMessage(t('socialForm.error.connection'));
|
||||||
}
|
|
||||||
System.setCookie('token', response, 30);
|
|
||||||
const token: string | null = System.getCookie('token');
|
|
||||||
if (!token) {
|
|
||||||
errorMessage(t('socialForm.error.connection'));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSession({isConnected: true, user: null, accessToken: token});
|
await handleLoginSuccess(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,41 +97,62 @@ export default function SocialForm() {
|
|||||||
errorMessage(t('socialForm.error.connection'));
|
errorMessage(t('socialForm.error.connection'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
System.setCookie('token', response, 30);
|
await handleLoginSuccess(response);
|
||||||
const token: string | null = System.getCookie('token');
|
}
|
||||||
if (!token) {
|
}
|
||||||
errorMessage(t('socialForm.error.connection'));
|
|
||||||
|
async function handleOAuthClick(provider: 'google' | 'facebook' | 'apple'): Promise<void> {
|
||||||
|
if (!window.electron) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electron.oauthLogin(provider, configs.baseUrl);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
if (result.error !== 'Window closed by user') {
|
||||||
|
errorMessage(t('socialForm.error.connection'));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSession({isConnected: true, user: null, accessToken: token});
|
|
||||||
|
if (result.code) {
|
||||||
|
if (provider === 'google') {
|
||||||
|
await handleGoogleLogin(result.code);
|
||||||
|
} else if (provider === 'facebook' && result.state) {
|
||||||
|
await handleFacebookLogin(result.code, result.state);
|
||||||
|
} else if (provider === 'apple' && result.state) {
|
||||||
|
await handleAppleLogin(result.code, result.state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage(t('socialForm.error.connection'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center gap-3">
|
<div className="flex justify-center gap-3">
|
||||||
<Link
|
<button
|
||||||
href={`https://www.facebook.com/v18.0/dialog/oauth?client_id=1015270470233591&redirect_uri=${configs.baseUrl}login?provider=facebook&scope=email&response_type=code&state=abc123`}
|
onClick={() => handleOAuthClick('facebook')}
|
||||||
className="flex items-center justify-center w-14 h-14 bg-[#1877F2] hover:bg-opacity-90 text-textPrimary rounded-xl transition-colors"
|
className="flex items-center justify-center w-14 h-14 bg-[#1877F2] hover:bg-opacity-90 text-textPrimary rounded-xl transition-colors cursor-pointer"
|
||||||
aria-label="Login with Facebook"
|
aria-label="Login with Facebook"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faFacebookF} className="w-6 h-6 text-textPrimary"/>
|
<FontAwesomeIcon icon={faFacebookF} className="w-6 h-6 text-textPrimary"/>
|
||||||
</Link>
|
</button>
|
||||||
|
|
||||||
<Link
|
<button
|
||||||
href={`https://accounts.google.com/o/oauth2/v2/auth?client_id=911482317931-pvjog1br22r6l8k1afq0ki94em2fsoen.apps.googleusercontent.com&redirect_uri=${configs.baseUrl}login?provider=google&response_type=code&scope=openid email profile&access_type=offline`}
|
onClick={() => handleOAuthClick('google')}
|
||||||
className="flex items-center justify-center w-14 h-14 bg-white hover:bg-opacity-90 text-[#4285F4] rounded-xl transition-colors"
|
className="flex items-center justify-center w-14 h-14 bg-white hover:bg-opacity-90 text-[#4285F4] rounded-xl transition-colors cursor-pointer"
|
||||||
aria-label="Login with Google"
|
aria-label="Login with Google"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faGoogle} className="w-6 h-6 text-[#4285F4]"/>
|
<FontAwesomeIcon icon={faGoogle} className="w-6 h-6 text-[#4285F4]"/>
|
||||||
</Link>
|
</button>
|
||||||
|
|
||||||
<Link
|
<button
|
||||||
href={`https://appleid.apple.com/auth/authorize?client_id=eritors.apple.login&redirect_uri=https://eritors.com/login?provider=apple&response_type=code&scope=email name&response_mode=form_post&state=abc123`}
|
onClick={() => handleOAuthClick('apple')}
|
||||||
className="flex items-center justify-center w-14 h-14 bg-black hover:bg-opacity-90 text-white rounded-xl transition-colors"
|
className="flex items-center justify-center w-14 h-14 bg-black hover:bg-opacity-90 text-white rounded-xl transition-colors cursor-pointer"
|
||||||
aria-label="Login with Apple"
|
aria-label="Login with Apple"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faApple} className="w-6 h-6 text-white"/>
|
<FontAwesomeIcon icon={faApple} className="w-6 h-6 text-white"/>
|
||||||
</Link>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
11
app/page.tsx
11
app/page.tsx
@@ -456,6 +456,7 @@ function ScribeContent() {
|
|||||||
user: user,
|
user: user,
|
||||||
accessToken: token,
|
accessToken: token,
|
||||||
});
|
});
|
||||||
|
console.log(user)
|
||||||
setCurrentCredits(user.creditsBalance)
|
setCurrentCredits(user.creditsBalance)
|
||||||
setAmountSpent(user.aiUsage)
|
setAmountSpent(user.aiUsage)
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
@@ -601,14 +602,8 @@ function ScribeContent() {
|
|||||||
<AutoSyncOnReconnect/>
|
<AutoSyncOnReconnect/>
|
||||||
<BookContext.Provider value={{book: currentBook, setBook: setCurrentBook}}>
|
<BookContext.Provider value={{book: currentBook, setBook: setCurrentBook}}>
|
||||||
<ChapterContext.Provider value={{chapter: currentChapter, setChapter: setCurrentChapter}}>
|
<ChapterContext.Provider value={{chapter: currentChapter, setChapter: setCurrentChapter}}>
|
||||||
<AIUsageContext.Provider value={{
|
<AIUsageContext.Provider value={{totalCredits: currentCredits, setTotalCredits: setCurrentCredits, totalPrice: amountSpent, setTotalPrice: setAmountSpent}}>
|
||||||
totalCredits: currentCredits,
|
<div className="bg-background text-text-primary h-screen flex flex-col font-['Lora']">
|
||||||
setTotalCredits: setCurrentCredits,
|
|
||||||
totalPrice: amountSpent,
|
|
||||||
setTotalPrice: setAmountSpent
|
|
||||||
}}>
|
|
||||||
<div
|
|
||||||
className="bg-background text-text-primary h-screen flex flex-col font-['Lora']">
|
|
||||||
<ScribeTopBar/>
|
<ScribeTopBar/>
|
||||||
<EditorContext.Provider value={{editor: editor}}>
|
<EditorContext.Provider value={{editor: editor}}>
|
||||||
<ScribeControllerBar/>
|
<ScribeControllerBar/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useContext} from "react";
|
import React, {useContext} from "react";
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
import {faCoins, faDollarSign} from "@fortawesome/free-solid-svg-icons";
|
import {faCoins, faDollarSign} from "@fortawesome/free-solid-svg-icons";
|
||||||
import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext";
|
import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext";
|
||||||
|
|||||||
8
electron.d.ts
vendored
8
electron.d.ts
vendored
@@ -40,6 +40,14 @@ export interface IElectronAPI {
|
|||||||
// Open external links (browser/native app)
|
// Open external links (browser/native app)
|
||||||
openExternal: (url: string) => Promise<void>;
|
openExternal: (url: string) => Promise<void>;
|
||||||
|
|
||||||
|
// OAuth login via BrowserWindow
|
||||||
|
oauthLogin: (provider: 'google' | 'facebook' | 'apple', baseUrl: string) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
code?: string;
|
||||||
|
state?: string;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
// Offline mode management
|
// Offline mode management
|
||||||
offlinePinSet: (pin: string) => Promise<{ success: boolean; error?: string }>;
|
offlinePinSet: (pin: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
offlinePinVerify: (pin: string) => Promise<{ success: boolean; userId?: string; error?: string }>;
|
offlinePinVerify: (pin: string) => Promise<{ success: boolean; userId?: string; error?: string }>;
|
||||||
|
|||||||
108
electron/main.ts
108
electron/main.ts
@@ -188,6 +188,114 @@ ipcMain.handle('open-external', async (_event, url: string) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// IPC Handler pour OAuth login via BrowserWindow
|
||||||
|
let oauthWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
|
interface OAuthResult {
|
||||||
|
success: boolean;
|
||||||
|
code?: string;
|
||||||
|
state?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OAuthRequest {
|
||||||
|
provider: 'google' | 'facebook' | 'apple';
|
||||||
|
baseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('oauth-login', async (_event, request: OAuthRequest): Promise<OAuthResult> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const { provider, baseUrl } = request;
|
||||||
|
const redirectUri = `${baseUrl}login?provider=${provider}`;
|
||||||
|
const encodedRedirectUri = encodeURIComponent(redirectUri);
|
||||||
|
|
||||||
|
// Fermer une éventuelle fenêtre OAuth existante
|
||||||
|
if (oauthWindow) {
|
||||||
|
oauthWindow.close();
|
||||||
|
oauthWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration OAuth par provider
|
||||||
|
const oauthConfigs: Record<string, string> = {
|
||||||
|
google: `https://accounts.google.com/o/oauth2/v2/auth?client_id=911482317931-pvjog1br22r6l8k1afq0ki94em2fsoen.apps.googleusercontent.com&redirect_uri=${encodedRedirectUri}&response_type=code&scope=openid%20email%20profile&access_type=offline`,
|
||||||
|
facebook: `https://www.facebook.com/v18.0/dialog/oauth?client_id=1015270470233591&redirect_uri=${encodedRedirectUri}&scope=email&response_type=code&state=abc123`,
|
||||||
|
apple: `https://appleid.apple.com/auth/authorize?client_id=eritors.apple.login&redirect_uri=${encodedRedirectUri}&response_type=code&scope=email%20name&response_mode=query&state=abc123`
|
||||||
|
};
|
||||||
|
|
||||||
|
const authUrl = oauthConfigs[provider];
|
||||||
|
if (!authUrl) {
|
||||||
|
resolve({ success: false, error: 'Invalid provider' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
oauthWindow = new BrowserWindow({
|
||||||
|
width: 600,
|
||||||
|
height: 700,
|
||||||
|
show: true,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Intercepter les redirections pour capturer le code OAuth
|
||||||
|
const handleNavigation = (url: string): boolean => {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
|
||||||
|
// Vérifier si c'est notre redirect URI (compare sans le query string)
|
||||||
|
const baseRedirectUri = `${baseUrl}login`;
|
||||||
|
if (url.startsWith(baseRedirectUri)) {
|
||||||
|
const code = parsedUrl.searchParams.get('code');
|
||||||
|
const state = parsedUrl.searchParams.get('state');
|
||||||
|
const error = parsedUrl.searchParams.get('error');
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
resolve({ success: false, error });
|
||||||
|
} else if (code) {
|
||||||
|
resolve({ success: true, code, state: state || undefined });
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: 'No code received' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oauthWindow) {
|
||||||
|
oauthWindow.close();
|
||||||
|
oauthWindow = null;
|
||||||
|
}
|
||||||
|
return true; // Navigation interceptée
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// URL invalide, continuer
|
||||||
|
}
|
||||||
|
return false; // Laisser la navigation continuer
|
||||||
|
};
|
||||||
|
|
||||||
|
// Écouter will-redirect (redirections HTTP)
|
||||||
|
oauthWindow.webContents.on('will-redirect', (event, url) => {
|
||||||
|
if (handleNavigation(url)) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Écouter will-navigate (navigations normales)
|
||||||
|
oauthWindow.webContents.on('will-navigate', (event, url) => {
|
||||||
|
if (handleNavigation(url)) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gérer la fermeture de la fenêtre par l'utilisateur
|
||||||
|
oauthWindow.on('closed', () => {
|
||||||
|
oauthWindow = null;
|
||||||
|
resolve({ success: false, error: 'Window closed by user' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Charger l'URL OAuth
|
||||||
|
oauthWindow.loadURL(authUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// IPC Handlers pour la gestion du token (OS-encrypted storage)
|
// IPC Handlers pour la gestion du token (OS-encrypted storage)
|
||||||
ipcMain.handle('get-token', () => {
|
ipcMain.handle('get-token', () => {
|
||||||
const storage:SecureStorage = getSecureStorage();
|
const storage:SecureStorage = getSecureStorage();
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ contextBridge.exposeInMainWorld('electron', {
|
|||||||
// Open external links (browser/native app)
|
// Open external links (browser/native app)
|
||||||
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
|
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
|
||||||
|
|
||||||
|
// OAuth login via BrowserWindow
|
||||||
|
oauthLogin: (provider: 'google' | 'facebook' | 'apple', baseUrl: string) =>
|
||||||
|
ipcRenderer.invoke('oauth-login', { provider, baseUrl }),
|
||||||
|
|
||||||
// Offline mode management
|
// Offline mode management
|
||||||
offlinePinSet: (pin: string) => ipcRenderer.invoke('offline:pin:set', { pin }),
|
offlinePinSet: (pin: string) => ipcRenderer.invoke('offline:pin:set', { pin }),
|
||||||
offlinePinVerify: (pin: string) => ipcRenderer.invoke('offline:pin:verify', { pin }),
|
offlinePinVerify: (pin: string) => ipcRenderer.invoke('offline:pin:verify', { pin }),
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export interface Conversation {
|
|||||||
type?: ConversationType;
|
type?: ConversationType;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
status: number;
|
status: number;
|
||||||
totalPrice?: number;
|
totalPrice?: number
|
||||||
|
useYourKey?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AIGeneratedText {
|
export interface AIGeneratedText {
|
||||||
@@ -34,21 +35,25 @@ export interface AIResponseWithCredits<T> {
|
|||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AIDictionary extends AIResponseWithCredits<DictionaryAIResponse> {}
|
export interface AIDictionary extends AIResponseWithCredits<DictionaryAIResponse> {
|
||||||
|
}
|
||||||
|
|
||||||
export interface AIGeneratedTextData {
|
export interface AIGeneratedTextData {
|
||||||
totalCost: number;
|
totalCost: number;
|
||||||
response: string;
|
response: string;
|
||||||
}
|
}
|
||||||
export interface AIGeneratedText extends AIResponseWithCredits<AIGeneratedTextData> {}
|
|
||||||
|
|
||||||
export interface AIInspire extends AIResponseWithCredits<InspireAIResponse> {}
|
export interface AIGeneratedText extends AIResponseWithCredits<AIGeneratedTextData> {
|
||||||
|
}
|
||||||
|
|
||||||
export interface AISynonyms extends AIResponseWithCredits<SynonymsAIResponse> {}
|
export interface AIInspire extends AIResponseWithCredits<InspireAIResponse> {
|
||||||
|
}
|
||||||
|
|
||||||
export interface AIVerbConjugation extends AIResponseWithCredits<unknown> {}
|
export interface AISynonyms extends AIResponseWithCredits<SynonymsAIResponse> {
|
||||||
|
}
|
||||||
|
|
||||||
export interface AISimpleText extends AIResponseWithCredits<string> {}
|
export interface AIVerbConjugation extends AIResponseWithCredits<unknown> {
|
||||||
|
}
|
||||||
|
|
||||||
interface InspireAIResponse {
|
interface InspireAIResponse {
|
||||||
ideas: {
|
ideas: {
|
||||||
@@ -80,10 +85,6 @@ export interface InspirationAIIdea {
|
|||||||
relatedTo: string;
|
relatedTo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InspiredAIResponse {
|
|
||||||
ideas: InspirationAIIdea[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConversationProps {
|
export interface ConversationProps {
|
||||||
id: string;
|
id: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
@@ -94,8 +95,13 @@ export interface ConversationProps {
|
|||||||
|
|
||||||
export default class QuillSense {
|
export default class QuillSense {
|
||||||
static getSubLevel(session: SessionProps): number {
|
static getSubLevel(session: SessionProps): number {
|
||||||
const currentSub: Subscription | null = User.getCurrentSubscription(session?.user, 'quill-sense');
|
let currentSub: Subscription | null = User.getCurrentSubscription(session?.user, 'quill-sense');
|
||||||
if (!currentSub) return 0;
|
if (!currentSub) {
|
||||||
|
currentSub = User.getCurrentSubscription(session?.user, 'quill-trial');
|
||||||
|
if (!currentSub) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
switch (currentSub?.subTier) {
|
switch (currentSub?.subTier) {
|
||||||
case 1:
|
case 1:
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const writingLevel: SelectBoxProps[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default class User {
|
export default class User {
|
||||||
static getCurrentSubscription(user: UserProps | null, type: "quill-sense" | "use-your-keys"): Subscription | null {
|
static getCurrentSubscription(user: UserProps | null, type: "quill-sense" | "use-your-keys" | "quill-trial"): Subscription | null {
|
||||||
if (!user || !user.subscription || user.subscription.length === 0) {
|
if (!user || !user.subscription || user.subscription.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user