diff --git a/src/api/users.ts b/src/api/users.ts index a7a4af1..6fb3841 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -119,11 +119,16 @@ export type GoogleOAuthFlowResponse = first_name: string; last_name: string; avatar_url: string; + resolution: "new_account" | "existing_email_claim"; + mobile_hint?: string | null; } | { status: "claim_required"; mobile: string; detail?: string; + email: string; + resolution: "existing_email_claim" | "existing_mobile_claim"; + mobile_hint?: string | null; }; export const getGoogleOAuthFlow = async (flow: string): Promise => { diff --git a/src/locales/en.ts b/src/locales/en.ts index 2b1557b..0af95fd 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -109,26 +109,31 @@ export const en = { countdownLabel: (time: string) => `Retry in ${time}`, fallback: "Too many requests. Please wait and try again.", }, - google: { - loadingTitle: "Completing Google sign in", - loadingDescription: "We are validating your Google account and preparing the next step.", - collectMobileTitle: "Finish your account setup", - collectMobileDescription: (email: string) => - `Google verified ${email}. Enter your mobile number to finish creating your account.`, - claimTitle: "Verify your existing account", - claimDescription: (mobile: string) => - `An account with ${mobile} already exists. Enter the verification code sent to that number to attach Google.`, - errorTitle: "Google sign in could not be completed", - missingFlow: "The Google sign-in flow is missing or has expired.", - loadFailed: "We could not load your Google sign-in state.", - completeFailed: "We could not finish your Google account setup.", - claimOtpSent: "Verification code sent successfully.", - googleAccount: "Google account", - completeButton: "Continue and create account", - verifyClaimButton: "Verify and continue", - resendClaimOtp: "Resend verification code", - restartGoogle: "Start Google sign in again", - }, + google: { + loadingTitle: "Completing Google sign in", + loadingDescription: "We are validating your Google account and preparing the next step.", + collectMobileTitle: "Finish your account setup", + collectMobileDescription: (email: string) => + `Google verified ${email}. Enter your mobile number to finish creating your account.`, + existingEmailClaimDescription: (email: string, mobileHint: string) => + `Google verified ${email}. Enter the mobile number already connected to this account (${mobileHint}) to confirm ownership.`, + claimTitle: "Verify your existing account", + claimDescription: (mobile: string) => + `An account with ${mobile} already exists. Enter the verification code sent to that number to attach Google.`, + mobileClaimDescription: (mobile: string) => + `A mobile-only account with ${mobile} already exists. Verify the code sent to that number to attach Google and keep that account.`, + errorTitle: "Google sign in could not be completed", + missingFlow: "The Google sign-in flow is missing or has expired.", + loadFailed: "We could not load your Google sign-in state.", + completeFailed: "We could not finish your Google account setup.", + claimOtpSent: "Verification code sent successfully.", + googleAccount: "Google account", + mobileHintLabel: (mobileHint: string) => `Expected mobile: ${mobileHint}`, + completeButton: "Continue and create account", + verifyClaimButton: "Verify and continue", + resendClaimOtp: "Resend verification code", + restartGoogle: "Start Google sign in again", + }, }, loginTerms: { diff --git a/src/locales/fa.ts b/src/locales/fa.ts index ae4a48e..224cd99 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -109,25 +109,30 @@ export const fa = { countdownLabel: (time: string) => `تلاش دوباره تا ${time}`, fallback: "درخواست‌های زیادی ارسال شده است. کمی صبر کنید و دوباره تلاش کنید.", }, - google: { - loadingTitle: "در حال تکمیل ورود با گوگل", - loadingDescription: "در حال بررسی حساب گوگل شما و آماده‌سازی مرحله بعد هستیم.", - collectMobileTitle: "ساخت حساب را کامل کنید", - collectMobileDescription: (email: string) => - `حساب گوگل ${email} تایید شد. برای تکمیل ساخت حساب، شماره موبایل خود را وارد کنید.`, - claimTitle: "حساب موجود خود را تایید کنید", - claimDescription: (mobile: string) => - `حسابی با شماره ${mobile} از قبل وجود دارد. کد ارسال‌شده به این شماره را وارد کنید تا گوگل به همان حساب متصل شود.`, - errorTitle: "ورود با گوگل کامل نشد", - missingFlow: "جریان ورود با گوگل پیدا نشد یا منقضی شده است.", - loadFailed: "بارگذاری وضعیت ورود با گوگل انجام نشد.", - completeFailed: "تکمیل ساخت حساب با گوگل انجام نشد.", - claimOtpSent: "کد تایید با موفقیت ارسال شد.", - googleAccount: "حساب گوگل", - completeButton: "ادامه و ایجاد حساب", - verifyClaimButton: "تایید و ادامه", - resendClaimOtp: "ارسال دوباره کد تایید", - restartGoogle: "شروع دوباره ورود با گوگل", + google: { + loadingTitle: "در حال تکمیل ورود با گوگل", + loadingDescription: "در حال بررسی حساب گوگل شما و آماده‌سازی مرحله بعد هستیم.", + collectMobileTitle: "ساخت حساب را کامل کنید", + collectMobileDescription: (email: string) => + `حساب گوگل ${email} تایید شد. برای تکمیل ساخت حساب، شماره موبایل خود را وارد کنید.`, + existingEmailClaimDescription: (email: string, mobileHint: string) => + `حساب گوگل ${email} تایید شد. برای تایید مالکیت، شماره موبایل متصل به این حساب (${mobileHint}) را وارد کنید.`, + claimTitle: "حساب موجود خود را تایید کنید", + claimDescription: (mobile: string) => + `حسابی با شماره ${mobile} از قبل وجود دارد. کد ارسال‌شده به این شماره را وارد کنید تا گوگل به همان حساب متصل شود.`, + mobileClaimDescription: (mobile: string) => + `حسابی بدون ایمیل با شماره ${mobile} از قبل وجود دارد. کد ارسال‌شده به این شماره را وارد کنید تا گوگل به همان حساب متصل شود.`, + errorTitle: "ورود با گوگل کامل نشد", + missingFlow: "جریان ورود با گوگل پیدا نشد یا منقضی شده است.", + loadFailed: "بارگذاری وضعیت ورود با گوگل انجام نشد.", + completeFailed: "تکمیل ساخت حساب با گوگل انجام نشد.", + claimOtpSent: "کد تایید با موفقیت ارسال شد.", + googleAccount: "حساب گوگل", + mobileHintLabel: (mobileHint: string) => `شماره مورد انتظار: ${mobileHint}`, + completeButton: "ادامه و ایجاد حساب", + verifyClaimButton: "تایید و ادامه", + resendClaimOtp: "ارسال دوباره کد تایید", + restartGoogle: "شروع دوباره ورود با گوگل", } }, diff --git a/src/pages/GoogleAuthCallback.tsx b/src/pages/GoogleAuthCallback.tsx index 4a2c240..7dec574 100644 --- a/src/pages/GoogleAuthCallback.tsx +++ b/src/pages/GoogleAuthCallback.tsx @@ -39,6 +39,10 @@ export default function GoogleAuthCallback() { const [mobile, setMobile] = useState(""); const [otpCode, setOtpCode] = useState(""); const [googleEmail, setGoogleEmail] = useState(""); + const [flowResolution, setFlowResolution] = useState< + "new_account" | "existing_email_claim" | "existing_mobile_claim" | null + >(null); + const [mobileHint, setMobileHint] = useState(null); const [errorMessage, setErrorMessage] = useState(""); const [cooldowns, setCooldowns] = useState>({ otpSend: 0, @@ -93,12 +97,19 @@ export default function GoogleAuthCallback() { if (payload.status === "collect_mobile") { setGoogleEmail(payload.email); + setFlowResolution(payload.resolution); + setMobileHint(payload.mobile_hint ?? null); + setErrorMessage(""); setStep("collect_mobile"); return; } if (payload.status === "claim_required") { setMobile(payload.mobile); + setGoogleEmail(payload.email); + setFlowResolution(payload.resolution); + setMobileHint(payload.mobile_hint ?? null); + setErrorMessage(""); setStep("claim_required"); } }; @@ -174,7 +185,14 @@ export default function GoogleAuthCallback() { toast.success(t.login.google.claimOtpSent); } } catch (error) { - toast.error(error instanceof Error ? error.message : t.login.google.completeFailed); + const message = error instanceof Error ? error.message : t.login.google.completeFailed; + setErrorMessage(message); + if (error instanceof ApiError) { + if (error.code === "google_email_mobile_conflict" || error.code === "google_mobile_belongs_to_other_email") { + setStep("collect_mobile"); + } + } + toast.error(message); } finally { setLoading(false); } @@ -261,12 +279,26 @@ export default function GoogleAuthCallback() {

{step === "loading" && t.login.google.loadingDescription} {step === "collect_mobile" && - t.login.google.collectMobileDescription(googleEmail || "-")} - {step === "claim_required" && t.login.google.claimDescription(mobile)} + (flowResolution === "existing_email_claim" + ? t.login.google.existingEmailClaimDescription(googleEmail || "-", mobileHint || "-") + : t.login.google.collectMobileDescription(googleEmail || "-"))} + {step === "claim_required" && + (flowResolution === "existing_email_claim" + ? t.login.google.claimDescription(mobileHint || mobile) + : t.login.google.mobileClaimDescription(mobile))} {step === "error" && (errorMessage || t.login.google.loadFailed)}

+ {errorMessage && step !== "error" && ( +
+
+ +

{errorMessage}

+
+
+ )} + {activeWarning && (
@@ -303,13 +335,21 @@ export default function GoogleAuthCallback() {

{googleEmail}

+ {flowResolution === "existing_email_claim" && mobileHint && ( +
+ {t.login.google.mobileHintLabel(mobileHint)} +
+ )} setMobile(event.target.value)} + onChange={(event) => { + setMobile(event.target.value) + setErrorMessage("") + }} maxLength={11} disabled={loading} className={`h-11 ${isRtl ? "text-end" : "text-start"}`} @@ -330,7 +370,9 @@ export default function GoogleAuthCallback() {

- {t.login.google.claimDescription(mobile)} + {flowResolution === "existing_email_claim" + ? t.login.google.claimDescription(mobileHint || mobile) + : t.login.google.mobileClaimDescription(mobile)}

setOtpCode(event.target.value)} + onChange={(event) => { + setOtpCode(event.target.value) + setErrorMessage("") + }} maxLength={6} disabled={loading} className="h-11 text-center text-lg tracking-widest"