From 130f2f3214875a11492cab2d8787489d89a49f49 Mon Sep 17 00:00:00 2001 From: Franco Date: Wed, 17 Dec 2025 16:55:56 +0800 Subject: [PATCH 01/66] onekeyid login page --- .../pages/CreateOrImportWallet.tsx | 57 +++++--- .../Onboardingv2/pages/OneKeyIDLoginPage.tsx | 127 ++++++++++++++++++ .../src/views/Onboardingv2/router/index.tsx | 11 ++ .../OneKeyIDLoginContent.tsx | 13 +- packages/shared/src/routes/onboardingv2.ts | 2 + 5 files changed, 183 insertions(+), 27 deletions(-) create mode 100644 packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx diff --git a/packages/kit/src/views/Onboardingv2/pages/CreateOrImportWallet.tsx b/packages/kit/src/views/Onboardingv2/pages/CreateOrImportWallet.tsx index e476af3023ca..71ef92019a2c 100644 --- a/packages/kit/src/views/Onboardingv2/pages/CreateOrImportWallet.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/CreateOrImportWallet.tsx @@ -10,7 +10,6 @@ import { AnimatePresence, Badge, Button, - Dialog, HeightTransition, Icon, Image, @@ -21,27 +20,21 @@ import { YStack, } from '@onekeyhq/components'; import { generateMnemonic } from '@onekeyhq/core/src/secret'; -import { EKeylessWalletEnableScene } from '@onekeyhq/shared/src/keylessWallet/keylessWalletConsts'; import { ETranslations } from '@onekeyhq/shared/src/locale'; import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; -import platformEnv from '@onekeyhq/shared/src/platformEnv'; import type { IOnboardingParamListV2 } from '@onekeyhq/shared/src/routes'; import { EModalRoutes, EOnboardingPages, EOnboardingPagesV2, } from '@onekeyhq/shared/src/routes'; -import { EPrimePages } from '@onekeyhq/shared/src/routes/prime'; import externalWalletLogoUtils from '@onekeyhq/shared/src/utils/externalWalletLogoUtils'; import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; import backgroundApiProxy from '../../../background/instance/backgroundApiProxy'; import { AccountSelectorProviderMirror } from '../../../components/AccountSelector'; import { useKeylessWallet } from '../../../components/KeylessWallet/useKeylessWallet'; -import { useOneKeyAuth } from '../../../components/OneKeyAuth/useOneKeyAuth'; import useAppNavigation from '../../../hooks/useAppNavigation'; -import { TermsAndPrivacy } from '../../Onboarding/pages/GetStarted/components'; -import { showOneKeyIDLoginDialog } from '../../Prime/components/OneKeyIDLoginDialog'; import { OnboardingLayout } from '../components/OnboardingLayout'; import { AnimatedDeviceAvatar } from './GetStarted'; @@ -148,12 +141,10 @@ function CreateOrImportWallet() { const { fullOptions } = route.params ?? {}; const [expanded, setExpanded] = useState(false); const [keylessExpanded, setKeylessExpanded] = useState(false); - const { enableKeylessWallet, enableKeylessWalletLoading } = - useKeylessWallet(); + const { enableKeylessWalletLoading } = useKeylessWallet(); const walletKeys = ['metamask', 'okx', 'rainbow', 'tokenpocket'] as const; const navigation = useAppNavigation(); - const { isLoggedIn } = useOneKeyAuth(); const handleExpand = useCallback(() => { setExpanded((prev) => !prev); @@ -197,11 +188,14 @@ function CreateOrImportWallet() { defaultLogger.account.wallet.onboard({ onboardMethod: 'connectHWWallet' }); }; - const handleKeylessWalletClick = useCallback(async () => { - await enableKeylessWallet({ - fromScene: EKeylessWalletEnableScene.Onboarding, - }); - }, [enableKeylessWallet]); + const handleKeylessWalletClick = useCallback(() => { + // await enableKeylessWallet({ + // fromScene: EKeylessWalletEnableScene.Onboarding, + // }); + + navigation.push(EOnboardingPagesV2.OneKeyIDLogin); + }, [navigation]); + return ( @@ -243,6 +237,39 @@ function CreateOrImportWallet() { color="$iconSubdued" /> + + + {[ + { + title: 'Highest security', + badge: 'success' as const, + }, + { title: 'For large assets' }, + { title: 'Private keys stay on device' }, + { + title: 'Ideal for long-term storage', + }, + { title: 'Protects against malware' }, + ].map((item, index) => ( + + {item.title} + + ))} + + + {intl.formatMessage({ + id: ETranslations.global_supports, + })} + + + + + + + ) : null} diff --git a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx new file mode 100644 index 000000000000..8829a0fe9606 --- /dev/null +++ b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx @@ -0,0 +1,127 @@ +import type { IKeyOfIcons } from '@onekeyhq/components'; +import { + Divider, + Form, + Icon, + Input, + Page, + SizableText, + XStack, + YStack, + useForm, +} from '@onekeyhq/components'; + +import { ListItem } from '../../../components/ListItem'; +import { OnboardingLayout } from '../components/OnboardingLayout'; + +function OptionItem({ + icon, + title, + onPress, +}: { + icon: IKeyOfIcons; + title: string; + onPress: () => void; +}) { + return ( + + + + + ); +} + +function OneKeyIDLoginPage() { + const form = useForm<{ email: string }>({ + defaultValues: { email: '' }, + mode: 'onSubmit', + }); + + return ( + + + + + + + + Select your email + + Add a wallet with Google, Apple, or enter your email + + + { + // TODO: Handle Google login + }} + /> + { + // TODO: Handle Apple login + }} + /> + + + + Or + + + +
+ + text?.trim() ?? text} + addOns={[ + { + label: 'Submit', + onPress: () => { + // TODO: Handle email submit + }, + }, + ]} + /> + +
+
+
+
+
+
+ ); +} + +export { OneKeyIDLoginPage as default }; diff --git a/packages/kit/src/views/Onboardingv2/router/index.tsx b/packages/kit/src/views/Onboardingv2/router/index.tsx index e31b669a3131..53ec3514a80e 100644 --- a/packages/kit/src/views/Onboardingv2/router/index.tsx +++ b/packages/kit/src/views/Onboardingv2/router/index.tsx @@ -131,6 +131,12 @@ const KeylessWalletCreation = LazyLoadPage( false, , ); +const OneKeyIDLogin = LazyLoadPage( + () => import('../pages/OneKeyIDLoginPage'), + undefined, + false, + , +); const hiddenHeaderOptions = { headerShown: false, @@ -245,4 +251,9 @@ export const OnboardingRouterV2: IModalFlowNavigatorConfig< component: KeylessWalletCreation, options: hiddenHeaderOptions, }, + { + name: EOnboardingPagesV2.OneKeyIDLogin, + component: OneKeyIDLogin, + options: hiddenHeaderOptions, + }, ]; diff --git a/packages/kit/src/views/Prime/components/OneKeyIDLoginDialog/OneKeyIDLoginContent.tsx b/packages/kit/src/views/Prime/components/OneKeyIDLoginDialog/OneKeyIDLoginContent.tsx index 81eb6ae58abe..a16367595600 100644 --- a/packages/kit/src/views/Prime/components/OneKeyIDLoginDialog/OneKeyIDLoginContent.tsx +++ b/packages/kit/src/views/Prime/components/OneKeyIDLoginDialog/OneKeyIDLoginContent.tsx @@ -3,14 +3,7 @@ import { useCallback } from 'react'; import { useIntl } from 'react-intl'; -import { - Form, - Icon, - Input, - SizableText, - YStack, - useForm, -} from '@onekeyhq/components'; +import { Form, Icon, Input, YStack, useForm } from '@onekeyhq/components'; import { ListItem } from '@onekeyhq/kit/src/components/ListItem'; import { ETranslations } from '@onekeyhq/shared/src/locale'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; @@ -131,10 +124,6 @@ export function OneKeyIDLoginContent({ /> - - OneKey ID is all you need to access all OneKey services and earn - referral rewards. - ); } diff --git a/packages/shared/src/routes/onboardingv2.ts b/packages/shared/src/routes/onboardingv2.ts index c65089945ead..64dad6e5ed80 100644 --- a/packages/shared/src/routes/onboardingv2.ts +++ b/packages/shared/src/routes/onboardingv2.ts @@ -41,6 +41,7 @@ export enum EOnboardingPagesV2 { ImportKeyTag = 'ImportKeyTag', KeylessWalletRecovery = 'KeylessWalletRecovery', KeylessWalletCreation = 'KeylessWalletCreation', + OneKeyIDLogin = 'OneKeyIDLogin', } interface IVerifyRecoveryPhraseParams { mnemonic: string; @@ -111,4 +112,5 @@ export type IOnboardingParamListV2 = { email?: string; mode?: EOnboardingV2KeylessWalletCreationMode; }; + [EOnboardingPagesV2.OneKeyIDLogin]: undefined; }; From 04313e2c07e75b13f0d40430fc22acceae6652ff Mon Sep 17 00:00:00 2001 From: Franco Date: Thu, 18 Dec 2025 14:54:19 +0800 Subject: [PATCH 02/66] social & otp --- .../Onboardingv2/pages/CreatePinPage.tsx | 28 ++ .../Onboardingv2/pages/OneKeyIDLoginPage.tsx | 302 ++++++++++++++---- .../src/views/Onboardingv2/router/index.tsx | 11 + packages/shared/src/routes/onboardingv2.ts | 12 +- 4 files changed, 285 insertions(+), 68 deletions(-) create mode 100644 packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx diff --git a/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx new file mode 100644 index 000000000000..116859f31535 --- /dev/null +++ b/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx @@ -0,0 +1,28 @@ +import { Page, SizableText, YStack } from '@onekeyhq/components'; + +import { OnboardingLayout } from '../components/OnboardingLayout'; + +function CreatePinPage() { + return ( + + + + + + + + Create your PIN + + Set a PIN to secure your wallet + + + {/* TODO: Add PIN input UI */} + + + + + + ); +} + +export { CreatePinPage as default }; diff --git a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx index 8829a0fe9606..ddcbc70281a1 100644 --- a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx @@ -1,25 +1,42 @@ -import type { IKeyOfIcons } from '@onekeyhq/components'; +import { useCallback, useState } from 'react'; + +import { useRoute } from '@react-navigation/core'; + +import type { IIconProps, IKeyOfIcons } from '@onekeyhq/components'; import { + AnimatePresence, + Button, Divider, Form, Icon, Input, + OTPInput, Page, SizableText, XStack, YStack, useForm, } from '@onekeyhq/components'; +import type { IOnboardingParamListV2 } from '@onekeyhq/shared/src/routes'; +import { + EOnboardingPagesV2, + EOneKeyIDLoginView, +} from '@onekeyhq/shared/src/routes'; import { ListItem } from '../../../components/ListItem'; +import useAppNavigation from '../../../hooks/useAppNavigation'; import { OnboardingLayout } from '../components/OnboardingLayout'; +import type { RouteProp } from '@react-navigation/core'; + function OptionItem({ icon, + iconProps, title, onPress, }: { icon: IKeyOfIcons; + iconProps?: IIconProps; title: string; onPress: () => void; }) { @@ -30,93 +47,244 @@ function OptionItem({ gap="$2" drillIn borderWidth={1} - borderColor="$borderStrong" - $platform-web={{ - boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.06)', + borderColor="$transparent" + bg="$bgStrong" + hoverStyle={{ + bg: '$bgStrongHover', }} - // $theme-dark={{ - // bg: '$neutral2', - // borderWidth: 1, - // borderColor: '$borderStrong', - // }} - // $platform-native={{ - // borderWidth: 1, - // borderColor: '$borderSubdued', + pressStyle={{ + bg: '$bgStrongActive', + }} + // $platform-web={{ + // boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.06)', // }} userSelect="none" onPress={onPress} > - + ); } -function OneKeyIDLoginPage() { +function LoginView({ + onEmailSubmit, +}: { + onEmailSubmit: (email: string) => void; +}) { const form = useForm<{ email: string }>({ defaultValues: { email: '' }, mode: 'onSubmit', }); + const handleSubmit = useCallback(async () => { + const isValid = await form.trigger('email'); + if (isValid) { + const email = form.getValues('email'); + onEmailSubmit(email); + } + }, [form, onEmailSubmit]); + + return ( + + { + // TODO: Handle Google login + }} + /> + { + // TODO: Handle Apple login + }} + /> + + + + Or + + + +
+ + text?.trim() ?? text} + onSubmitEditing={() => handleSubmit()} + addOns={[ + { + label: 'Submit', + onPress: handleSubmit, + }, + ]} + /> + +
+
+ ); +} + +function VerifyView({ + email, + onConfirmSuccess, +}: { + email: string; + onConfirmSuccess: () => void; +}) { + const [verificationCode, setVerificationCode] = useState(''); + const [status, setStatus] = useState<'initial' | 'error'>('initial'); + const [isSubmitting, setIsSubmitting] = useState(false); + const [countdown, setCountdown] = useState(60); + + const handleConfirm = useCallback(async () => { + if (isSubmitting || verificationCode.length !== 6) { + return; + } + setIsSubmitting(true); + try { + // TODO: Implement OTP verification logic + console.log('Verifying code:', verificationCode, 'for email:', email); + onConfirmSuccess(); + } catch { + setStatus('error'); + } finally { + setIsSubmitting(false); + } + }, [isSubmitting, verificationCode, email, onConfirmSuccess]); + + const handleResend = useCallback(() => { + // TODO: Implement resend logic + setCountdown(60); + setStatus('initial'); + setVerificationCode(''); + console.log('Resending code to:', email); + }, [email]); + + // 1-0 + // 2-0.5 + // 3-1 + // 4-2 + // 5-5 + // 6-5 + // 7-forever + // + + return ( + + + { + setVerificationCode(value); + setStatus('initial'); + }} + /> + {status === 'error' ? ( + + Invalid verification code + + ) : null} + + + + + + + ); +} + +function OneKeyIDLoginPage() { + const navigation = useAppNavigation(); + const route = + useRoute< + RouteProp + >(); + const { initialView, email: initialEmail } = route.params ?? {}; + + const [view, setView] = useState( + initialView ?? EOneKeyIDLoginView.Login, + ); + const [email, setEmail] = useState(initialEmail ?? ''); + + const handleEmailSubmit = useCallback((submittedEmail: string) => { + setEmail(submittedEmail); + setView(EOneKeyIDLoginView.Verify); + // TODO: Send verification code to email + }, []); + + const handleConfirmSuccess = useCallback(() => { + navigation.push(EOnboardingPagesV2.CreatePin); + }, [navigation]); + return ( - - - - Select your email - - Add a wallet with Google, Apple, or enter your email - - - { - // TODO: Handle Google login - }} - /> - { - // TODO: Handle Apple login - }} - /> - - - - Or - - - -
- - text?.trim() ?? text} - addOns={[ - { - label: 'Submit', - onPress: () => { - // TODO: Handle email submit - }, - }, - ]} - /> - -
+ + + + {view === EOneKeyIDLoginView.Login + ? 'Select your email' + : 'Enter verification code'} + + + {view === EOneKeyIDLoginView.Login + ? 'Add a wallet with Google, Apple, or enter your email' + : 'A verification code was sent to your email'} + + + {view === EOneKeyIDLoginView.Login ? ( + + ) : ( + + )} +
diff --git a/packages/kit/src/views/Onboardingv2/router/index.tsx b/packages/kit/src/views/Onboardingv2/router/index.tsx index 53ec3514a80e..60a18accecac 100644 --- a/packages/kit/src/views/Onboardingv2/router/index.tsx +++ b/packages/kit/src/views/Onboardingv2/router/index.tsx @@ -137,6 +137,12 @@ const OneKeyIDLogin = LazyLoadPage( false, , ); +const CreatePin = LazyLoadPage( + () => import('../pages/CreatePinPage'), + undefined, + false, + , +); const hiddenHeaderOptions = { headerShown: false, @@ -256,4 +262,9 @@ export const OnboardingRouterV2: IModalFlowNavigatorConfig< component: OneKeyIDLogin, options: hiddenHeaderOptions, }, + { + name: EOnboardingPagesV2.CreatePin, + component: CreatePin, + options: hiddenHeaderOptions, + }, ]; diff --git a/packages/shared/src/routes/onboardingv2.ts b/packages/shared/src/routes/onboardingv2.ts index 64dad6e5ed80..78f9696b229d 100644 --- a/packages/shared/src/routes/onboardingv2.ts +++ b/packages/shared/src/routes/onboardingv2.ts @@ -19,6 +19,11 @@ export enum EOnboardingV2KeylessWalletCreationMode { View = 'View', } +export enum EOneKeyIDLoginView { + Login = 'login', + Verify = 'verify', +} + export enum EOnboardingPagesV2 { GetStarted = 'GetStarted', AddExistingWallet = 'AddExistingWallet', @@ -42,6 +47,7 @@ export enum EOnboardingPagesV2 { KeylessWalletRecovery = 'KeylessWalletRecovery', KeylessWalletCreation = 'KeylessWalletCreation', OneKeyIDLogin = 'OneKeyIDLogin', + CreatePin = 'CreatePin', } interface IVerifyRecoveryPhraseParams { mnemonic: string; @@ -112,5 +118,9 @@ export type IOnboardingParamListV2 = { email?: string; mode?: EOnboardingV2KeylessWalletCreationMode; }; - [EOnboardingPagesV2.OneKeyIDLogin]: undefined; + [EOnboardingPagesV2.OneKeyIDLogin]: { + initialView?: EOneKeyIDLoginView; + email?: string; + }; + [EOnboardingPagesV2.CreatePin]: undefined; }; From f91e280f70364dd09fa8f62715a0aa1f497936c6 Mon Sep 17 00:00:00 2001 From: Franco Date: Mon, 22 Dec 2025 11:21:01 +0800 Subject: [PATCH 03/66] ui --- .../src/primitives/Button/index.tsx | 2 +- .../Password/components/PasswordSetup.tsx | 61 ++-- .../components/PinInputLayout.tsx | 142 ++++++++ .../Onboardingv2/pages/ConfirmPinPage.tsx | 64 ++++ .../Onboardingv2/pages/CreatePasscodePage.tsx | 61 ++++ .../Onboardingv2/pages/CreatePinPage.tsx | 49 +-- .../views/Onboardingv2/pages/GetStarted.tsx | 23 +- .../Onboardingv2/pages/OneKeyIDLoginPage.tsx | 334 ++++++------------ .../views/Onboardingv2/pages/ResetPinPage.tsx | 89 +++++ .../Onboardingv2/pages/VerifyPinPage.tsx | 148 ++++++++ .../src/views/Onboardingv2/router/index.tsx | 44 +++ packages/shared/src/routes/onboardingv2.ts | 20 +- 12 files changed, 742 insertions(+), 295 deletions(-) create mode 100644 packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx create mode 100644 packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx create mode 100644 packages/kit/src/views/Onboardingv2/pages/CreatePasscodePage.tsx create mode 100644 packages/kit/src/views/Onboardingv2/pages/ResetPinPage.tsx create mode 100644 packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx diff --git a/packages/components/src/primitives/Button/index.tsx b/packages/components/src/primitives/Button/index.tsx index 9c0531b090fa..a0175a51f7d4 100644 --- a/packages/components/src/primitives/Button/index.tsx +++ b/packages/components/src/primitives/Button/index.tsx @@ -129,7 +129,7 @@ export const getSharedButtonStyles = ({ }, } : { - opacity: 0.5, + opacity: 0.4, }), }; diff --git a/packages/kit/src/components/Password/components/PasswordSetup.tsx b/packages/kit/src/components/Password/components/PasswordSetup.tsx index 4f5ad33996e2..1506066aedbb 100644 --- a/packages/kit/src/components/Password/components/PasswordSetup.tsx +++ b/packages/kit/src/components/Password/components/PasswordSetup.tsx @@ -42,6 +42,8 @@ interface IPasswordSetupProps { onSetupPassword: (data: IPasswordSetupForm) => void; biologyAuthSwitchContainer?: React.ReactNode; confirmBtnText?: string; + pageMode?: boolean; + onStepChange?: (step: 'create' | 'confirm') => void; } const PasswordSetup = ({ @@ -50,6 +52,8 @@ const PasswordSetup = ({ onSetupPassword, confirmBtnText, biologyAuthSwitchContainer, + pageMode, + onStepChange, }: IPasswordSetupProps) => { const intl = useIntl(); const [currentPasswordMode, setCurrentPasswordMode] = useState(passwordMode); @@ -86,6 +90,7 @@ const PasswordSetup = ({ }, [confirmBtnText, intl, passCodeFirstStep]); const onPassCodeNext = () => { setPassCodeConfirm(true); + onStepChange?.('confirm'); setTimeout(() => { form.setFocus('confirmPassCode'); }, 150); @@ -101,33 +106,29 @@ const PasswordSetup = ({ return ( <> - {currentPasswordMode === EPasswordMode.PASSCODE && passCodeConfirm ? ( + {!pageMode ? ( {intl.formatMessage({ - id: ETranslations.auth_confirm_passcode_form_label, + id: + currentPasswordMode === EPasswordMode.PASSCODE && + passCodeConfirm + ? ETranslations.auth_confirm_passcode_form_label + : ETranslations.global_set_passcode, })} - ) : ( - - - - {intl.formatMessage({ - id: ETranslations.global_set_passcode, - })} - - - - )} + ) : null}
{currentPasswordMode === EPasswordMode.PASSWORD ? ( <> void; + value: string; + onChange: (pin: string) => void; + onSubmit: () => void; + isSubmitDisabled?: boolean; + isInputDisabled?: boolean; + errorMessage?: string; +} + +function PinInputLayout({ + title, + description, + descriptionColor = '$textSubdued', + buttonText, + secondaryButtonText, + onSecondaryButtonPress, + value, + onChange, + onSubmit, + isSubmitDisabled = false, + isInputDisabled = false, + errorMessage, +}: IPinInputLayoutProps) { + const inputRef = useRef(null); + + useFocusEffect( + useCallback(() => { + const timer = setTimeout(() => { + inputRef.current?.focus(); + }, 300); + return () => clearTimeout(timer); + }, []), + ); + + const handleChangeText = useCallback( + (text: string) => { + onChange(text.replace(/[^0-9]/g, '')); + }, + [onChange], + ); + + const handleSubmitEditing = useCallback(() => { + if (!isSubmitDisabled) { + onSubmit(); + } + }, [isSubmitDisabled, onSubmit]); + + return ( + + + + + + + {title} + + {description} + + + + + + + + {errorMessage ? ( + + {errorMessage} + + ) : null} + + + + {secondaryButtonText && onSecondaryButtonPress ? ( + + ) : null} + + + + + + + + ); +} + +export { PinInputLayout }; diff --git a/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx new file mode 100644 index 000000000000..5898d473e51d --- /dev/null +++ b/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx @@ -0,0 +1,64 @@ +import { useCallback, useState } from 'react'; + +import { useRoute } from '@react-navigation/core'; + +import type { IOnboardingParamListV2 } from '@onekeyhq/shared/src/routes'; +import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; + +import useAppNavigation from '../../../hooks/useAppNavigation'; +import { PinInputLayout } from '../components/PinInputLayout'; + +import type { RouteProp } from '@react-navigation/core'; + +function ConfirmPinPage() { + const navigation = useAppNavigation(); + const route = + useRoute< + RouteProp + >(); + const { pin: originalPin } = route.params; + + const [confirmPin, setConfirmPin] = useState(''); + const [isValid, setIsValid] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + + const handlePinChange = useCallback( + (filteredText: string) => { + setConfirmPin(filteredText); + setErrorMessage(''); + + // Auto-validate when 4 digits entered + if (filteredText.length === 4) { + if (filteredText === originalPin) { + setIsValid(true); + } else { + setErrorMessage('Incorrect PIN. Please try again.'); + setIsValid(false); + } + } else { + setIsValid(false); + } + }, + [originalPin], + ); + + const handleConfirm = useCallback(() => { + navigation.push(EOnboardingPagesV2.CreatePasscode); + }, [navigation]); + + return ( + + ); +} + +export { ConfirmPinPage as default }; diff --git a/packages/kit/src/views/Onboardingv2/pages/CreatePasscodePage.tsx b/packages/kit/src/views/Onboardingv2/pages/CreatePasscodePage.tsx new file mode 100644 index 000000000000..053d1ae8d32b --- /dev/null +++ b/packages/kit/src/views/Onboardingv2/pages/CreatePasscodePage.tsx @@ -0,0 +1,61 @@ +import { useCallback, useState } from 'react'; + +import { useIntl } from 'react-intl'; + +import { Page, SizableText, YStack } from '@onekeyhq/components'; +import { usePasswordModeAtom } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; + +import PasswordSetup from '../../../components/Password/components/PasswordSetup'; +import { OnboardingLayout } from '../components/OnboardingLayout'; + +import type { IPasswordSetupForm } from '../../../components/Password/components/PasswordSetup'; + +function CreatePasscodePage() { + const intl = useIntl(); + const [loading, setLoading] = useState(false); + const [step, setStep] = useState<'create' | 'confirm'>('create'); + const [passwordMode] = usePasswordModeAtom(); + + const handleSetupPasscode = useCallback((data: IPasswordSetupForm) => { + setLoading(true); + // TODO: Handle passcode setup + console.log('Passcode setup:', data); + setLoading(false); + }, []); + + return ( + + + + + + + + {step === 'create' + ? intl.formatMessage({ + id: ETranslations.global_set_passcode, + }) + : intl.formatMessage({ + id: ETranslations.auth_confirm_passcode_form_label, + })} + + + You will use this to unlock your wallet. + + + + + + + + ); +} + +export { CreatePasscodePage as default }; diff --git a/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx index 116859f31535..ed7dbef16e5b 100644 --- a/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx @@ -1,27 +1,36 @@ -import { Page, SizableText, YStack } from '@onekeyhq/components'; +import { useCallback, useState } from 'react'; -import { OnboardingLayout } from '../components/OnboardingLayout'; +import { SizableText } from '@onekeyhq/components'; +import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; + +import useAppNavigation from '../../../hooks/useAppNavigation'; +import { PinInputLayout } from '../components/PinInputLayout'; function CreatePinPage() { + const navigation = useAppNavigation(); + const [pin, setPin] = useState(''); + + const handleContinue = useCallback(() => { + navigation.push(EOnboardingPagesV2.ConfirmPin, { pin }); + }, [navigation, pin]); + return ( - - - - - - - - Create your PIN - - Set a PIN to secure your wallet - - - {/* TODO: Add PIN input UI */} - - - - - + + This is used to secure your wallet on all your devices.{' '} + + This cannot be recovered. + + + } + buttonText="Continue" + value={pin} + onChange={setPin} + onSubmit={handleContinue} + isSubmitDisabled={pin.length !== 4} + /> ); } diff --git a/packages/kit/src/views/Onboardingv2/pages/GetStarted.tsx b/packages/kit/src/views/Onboardingv2/pages/GetStarted.tsx index e4c779c6cd53..87684b7191ed 100644 --- a/packages/kit/src/views/Onboardingv2/pages/GetStarted.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/GetStarted.tsx @@ -1,4 +1,4 @@ -import { memo, useEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { EDeviceType } from '@onekeyfe/hd-shared'; import { MotiView } from 'moti'; @@ -20,6 +20,7 @@ import { Icon, Page, SizableText, + Spinner, Stack, XStack, YStack, @@ -311,6 +312,17 @@ export default function GetStarted() { navigation.push(EOnboardingPagesV2.CreateOrImportWallet); }; + const [isGoogleLoading, setIsGoogleLoading] = useState(false); + + const handleGoogleLogin = useCallback(() => { + setIsGoogleLoading(true); + // TODO: Implement actual Google login + setTimeout(() => { + setIsGoogleLoading(false); + navigation.push(EOnboardingPagesV2.VerifyPin); + }, 1000); + }, [navigation]); + // Cache theme values to avoid multiple useThemeValue calls during render const neutral6 = useThemeValue('$neutral6'); const bgColor = useThemeValue('$bgApp'); @@ -402,7 +414,7 @@ export default function GetStarted() { - + - - - + + + + {title} + + + + {isLoading ? ( + + + + ) : ( + + + + )} + + + ); } function OneKeyIDLoginPage() { const navigation = useAppNavigation(); - const route = - useRoute< - RouteProp - >(); - const { initialView, email: initialEmail } = route.params ?? {}; - - const [view, setView] = useState( - initialView ?? EOneKeyIDLoginView.Login, - ); - const [email, setEmail] = useState(initialEmail ?? ''); + const [isLoggingIn, setIsLoggingIn] = useState(false); - const handleEmailSubmit = useCallback((submittedEmail: string) => { - setEmail(submittedEmail); - setView(EOneKeyIDLoginView.Verify); - // TODO: Send verification code to email - }, []); + const handleGoogleLogin = useCallback(() => { + setIsLoggingIn(true); - const handleConfirmSuccess = useCallback(() => { - navigation.push(EOnboardingPagesV2.CreatePin); + setTimeout(() => { + setIsLoggingIn(false); + navigation.push(EOnboardingPagesV2.CreatePin); + }, 1000); }, [navigation]); return ( @@ -263,30 +115,42 @@ function OneKeyIDLoginPage() { - - {view === EOneKeyIDLoginView.Login - ? 'Select your email' - : 'Enter verification code'} - + Select your email - {view === EOneKeyIDLoginView.Login - ? 'Add a wallet with Google, Apple, or enter your email' - : 'A verification code was sent to your email'} + Add a wallet with your Google or Apple account - - {view === EOneKeyIDLoginView.Login ? ( - - ) : ( - - )} - + + + { + // TODO: Handle Apple login + }} + /> + + + + TODO — Link to "How to create wallet with Apple or Google account" + +
); diff --git a/packages/kit/src/views/Onboardingv2/pages/ResetPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/ResetPinPage.tsx new file mode 100644 index 000000000000..9cf2dd4207b2 --- /dev/null +++ b/packages/kit/src/views/Onboardingv2/pages/ResetPinPage.tsx @@ -0,0 +1,89 @@ +import { useCallback } from 'react'; + +import { + Button, + Page, + SizableText, + XStack, + YStack, +} from '@onekeyhq/components'; + +import useAppNavigation from '../../../hooks/useAppNavigation'; +import { OnboardingLayout } from '../components/OnboardingLayout'; + +const STEPS = [ + { + title: 'Open Other Device', + description: + 'Go to another device where your OneKey account is signed in with your email', + }, + { + title: 'Go to Settings', + description: 'In Settings, select "Security" and then "Reset PIN"', + }, + { + title: 'Set Your New PIN', + description: + "Once you've set your new PIN, you can now login to your wallet on this device", + }, +]; + +function ResetPinPage() { + const navigation = useAppNavigation(); + + const handleDone = useCallback(() => { + navigation.pop(); + }, [navigation]); + return ( + + + + + + + + Reset PIN using another device + + + For security, you can only reset your PIN in other devices where + you are logged in + + + + {STEPS.map((step, index) => ( + + + + {index + 1} + + + + + {step.title} + + + {step.description} + + + + ))} + + + + + + + + ); +} + +export { ResetPinPage as default }; diff --git a/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx new file mode 100644 index 000000000000..c2515053e652 --- /dev/null +++ b/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx @@ -0,0 +1,148 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; + +import useAppNavigation from '../../../hooks/useAppNavigation'; +import { PinInputLayout } from '../components/PinInputLayout'; + +const MAX_ATTEMPTS = 7; + +// Cooldown times based on attempt number (in seconds) +// Attempt 1: 0, Attempt 2: 30s, Attempt 3: 60s, Attempt 4: 120s, Attempt 5-6: 300s +const COOLDOWN_BY_ATTEMPT: Record = { + 1: 0, // First attempt: no cooldown + 2: 30, // 0.5 min + 3: 60, // 1 min + 4: 120, // 2 min + 5: 300, // 5 min + 6: 300, // 5 min +}; + +function VerifyPinPage() { + const navigation = useAppNavigation(); + const [pin, setPin] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [attemptsRemaining, setAttemptsRemaining] = useState(MAX_ATTEMPTS); + const [cooldownSeconds, setCooldownSeconds] = useState(0); + const [showAttemptError, setShowAttemptError] = useState(false); + const cooldownTimerRef = useRef | null>(null); + + const isInputDisabled = cooldownSeconds > 0; + + // Clear cooldown timer on unmount + useEffect(() => { + return () => { + if (cooldownTimerRef.current) { + clearInterval(cooldownTimerRef.current); + } + }; + }, []); + + const startCooldown = useCallback((seconds: number) => { + if (seconds <= 0) { + return; + } + setCooldownSeconds(seconds); + setPin(''); + + cooldownTimerRef.current = setInterval(() => { + setCooldownSeconds((prev) => { + if (prev <= 1) { + if (cooldownTimerRef.current) { + clearInterval(cooldownTimerRef.current); + cooldownTimerRef.current = null; + } + return 0; + } + return prev - 1; + }); + }, 1000); + }, []); + + const formatCooldownTime = useCallback((seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; + }, []); + + const handlePinChange = useCallback( + (filteredText: string) => { + if (isInputDisabled) { + return; + } + setPin(filteredText); + setErrorMessage(''); + setShowAttemptError(false); + }, + [isInputDisabled], + ); + + const handleVerify = useCallback(() => { + // TODO: Verify against actual stored PIN on server + const isCorrect = false; // Mock: always fail for testing + + if (isCorrect) { + navigation.push(EOnboardingPagesV2.CreatePasscode); + } else { + const newAttemptsRemaining = attemptsRemaining - 1; + const attemptNumber = MAX_ATTEMPTS - newAttemptsRemaining; + + setAttemptsRemaining(newAttemptsRemaining); + setPin(''); + setShowAttemptError(true); + + if (newAttemptsRemaining <= 0) { + // Max attempts reached - redirect to reset PIN page + navigation.replace(EOnboardingPagesV2.ResetPin); + } else { + // Get cooldown time for this attempt + const cooldownTime = COOLDOWN_BY_ATTEMPT[attemptNumber] || 0; + if (cooldownTime > 0) { + startCooldown(cooldownTime); + } + } + } + }, [attemptsRemaining, navigation, startCooldown]); + + const handleForgotPin = useCallback(() => { + navigation.push(EOnboardingPagesV2.ResetPin); + }, [navigation]); + + // Build error message based on state + const displayErrorMessage = (() => { + if (errorMessage) { + return errorMessage; + } + if ( + showAttemptError && + attemptsRemaining < MAX_ATTEMPTS && + attemptsRemaining > 0 + ) { + const baseMessage = `Incorrect PIN entered. ${attemptsRemaining} attempts remaining.`; + if (cooldownSeconds > 0) { + return `${baseMessage} Try again in ${formatCooldownTime( + cooldownSeconds, + )}.`; + } + return baseMessage; + } + return ''; + })(); + + return ( + + ); +} + +export { VerifyPinPage as default }; diff --git a/packages/kit/src/views/Onboardingv2/router/index.tsx b/packages/kit/src/views/Onboardingv2/router/index.tsx index 60a18accecac..0dbf2b6b1419 100644 --- a/packages/kit/src/views/Onboardingv2/router/index.tsx +++ b/packages/kit/src/views/Onboardingv2/router/index.tsx @@ -143,6 +143,30 @@ const CreatePin = LazyLoadPage( false, , ); +const ConfirmPin = LazyLoadPage( + () => import('../pages/ConfirmPinPage'), + undefined, + false, + , +); +const CreatePasscode = LazyLoadPage( + () => import('../pages/CreatePasscodePage'), + undefined, + false, + , +); +const VerifyPin = LazyLoadPage( + () => import('../pages/VerifyPinPage'), + undefined, + false, + , +); +const ResetPin = LazyLoadPage( + () => import('../pages/ResetPinPage'), + undefined, + false, + , +); const hiddenHeaderOptions = { headerShown: false, @@ -267,4 +291,24 @@ export const OnboardingRouterV2: IModalFlowNavigatorConfig< component: CreatePin, options: hiddenHeaderOptions, }, + { + name: EOnboardingPagesV2.ConfirmPin, + component: ConfirmPin, + options: hiddenHeaderOptions, + }, + { + name: EOnboardingPagesV2.CreatePasscode, + component: CreatePasscode, + options: hiddenHeaderOptions, + }, + { + name: EOnboardingPagesV2.VerifyPin, + component: VerifyPin, + options: hiddenHeaderOptions, + }, + { + name: EOnboardingPagesV2.ResetPin, + component: ResetPin, + options: hiddenHeaderOptions, + }, ]; diff --git a/packages/shared/src/routes/onboardingv2.ts b/packages/shared/src/routes/onboardingv2.ts index 78f9696b229d..36f4c63251b4 100644 --- a/packages/shared/src/routes/onboardingv2.ts +++ b/packages/shared/src/routes/onboardingv2.ts @@ -19,11 +19,6 @@ export enum EOnboardingV2KeylessWalletCreationMode { View = 'View', } -export enum EOneKeyIDLoginView { - Login = 'login', - Verify = 'verify', -} - export enum EOnboardingPagesV2 { GetStarted = 'GetStarted', AddExistingWallet = 'AddExistingWallet', @@ -48,6 +43,10 @@ export enum EOnboardingPagesV2 { KeylessWalletCreation = 'KeylessWalletCreation', OneKeyIDLogin = 'OneKeyIDLogin', CreatePin = 'CreatePin', + ConfirmPin = 'ConfirmPin', + CreatePasscode = 'CreatePasscode', + VerifyPin = 'VerifyPin', + ResetPin = 'ResetPin', } interface IVerifyRecoveryPhraseParams { mnemonic: string; @@ -118,9 +117,12 @@ export type IOnboardingParamListV2 = { email?: string; mode?: EOnboardingV2KeylessWalletCreationMode; }; - [EOnboardingPagesV2.OneKeyIDLogin]: { - initialView?: EOneKeyIDLoginView; - email?: string; - }; + [EOnboardingPagesV2.OneKeyIDLogin]: undefined; [EOnboardingPagesV2.CreatePin]: undefined; + [EOnboardingPagesV2.ConfirmPin]: { + pin: string; + }; + [EOnboardingPagesV2.CreatePasscode]: undefined; + [EOnboardingPagesV2.VerifyPin]: undefined; + [EOnboardingPagesV2.ResetPin]: undefined; }; From fedb6dc377cf9ec8d4b72f306171dabcb5b5a752 Mon Sep 17 00:00:00 2001 From: morizon Date: Sat, 20 Dec 2025 16:55:57 +0800 Subject: [PATCH 04/66] refactor: rename setMainWindow function for clarity and update related references - Renamed `setMainWindow` to `setMainWindowForOAuthServer` in `oauthLocalServer` for better clarity. - Updated all references in `app.ts` to reflect the new function name. - Enhanced the `BaseSkeleton` component to accept a forwarded ref, improving its integration with parent components. - Updated `HyperlinkText` to use a lazy-loaded default internationalization instance. - Improved OAuth state handling in `openOAuthPopupWeb` and `useSupabaseAuth` for enhanced security and reliability. --- apps/desktop/app/app.ts | 4 +- .../oauthLocalServer/oauthLocalServer.ts | 2 +- .../Skeleton/BaseSkeleton.native.tsx | 11 ++--- .../src/primitives/Skeleton/BaseSkeleton.tsx | 12 +++--- .../src/components/HyperlinkText/index.tsx | 14 +++++-- .../OneKeyAuth/openOAuthPopupWeb.tsx | 42 ++++++++++++++++++- .../OneKeyAuth/supabase/useSupabaseAuth.tsx | 32 ++++++++++++++ packages/kit/src/provider/Container/index.tsx | 2 +- .../ReferAFriendIntroPhase/index.tsx | 3 +- 9 files changed, 101 insertions(+), 21 deletions(-) diff --git a/apps/desktop/app/app.ts b/apps/desktop/app/app.ts index 11248fbd1e16..9c05a20369da 100644 --- a/apps/desktop/app/app.ts +++ b/apps/desktop/app/app.ts @@ -55,7 +55,7 @@ import { } from './resoucePath'; import { initSentry } from './sentry'; import { startServices } from './service'; -import { setMainWindow } from './service/oauthLocalServer/oauthLocalServer'; +import { setMainWindowForOAuthServer } from './service/oauthLocalServer/oauthLocalServer'; logger.initialize(); logger.transports.file.maxSize = 1024 * 1024 * 10; @@ -564,7 +564,7 @@ async function createMainWindow() { void browserWindow.loadURL(src); // Set main window reference for OAuth server - setMainWindow(browserWindow); + setMainWindowForOAuthServer(browserWindow); // Protocol handler for win32 if (isWin || isMac) { diff --git a/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts b/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts index b5819b0ce8f5..10f4467001a3 100644 --- a/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts +++ b/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts @@ -22,7 +22,7 @@ let oauthServer: Server | null = null; // Get main window reference (will be set from app.ts) let mainWindow: BrowserWindow | null = null; -export function setMainWindow(window: BrowserWindow | null) { +export function setMainWindowForOAuthServer(window: BrowserWindow | null) { mainWindow = window; } diff --git a/packages/components/src/primitives/Skeleton/BaseSkeleton.native.tsx b/packages/components/src/primitives/Skeleton/BaseSkeleton.native.tsx index f5d683864131..4f85cbb06e90 100644 --- a/packages/components/src/primitives/Skeleton/BaseSkeleton.native.tsx +++ b/packages/components/src/primitives/Skeleton/BaseSkeleton.native.tsx @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import type { ComponentRef, ForwardedRef } from 'react'; import { SkeletonView } from '@onekeyfe/react-native-skeleton'; @@ -18,11 +19,10 @@ const baseColors = { dark: ['#111111', '#333333'], light: ['#fafafa', '#cdcdcd'], }; -export function BaseSkeleton({ - colorMode, - children, - ...props -}: ISkeletonProps) { +export function BaseSkeleton( + { colorMode, children, ...props }: ISkeletonProps, + ref: ForwardedRef>, +) { const [restProps, style] = usePropsAndStyle(props, { resolveValues: 'auto', }); @@ -43,6 +43,7 @@ export function BaseSkeleton({ const isGroupLoading = useIsGroupLoading(); return isGroupLoading === undefined || isGroupLoading ? ( >, +) { const [{ className: classNameProp, ...restProps }, style] = usePropsAndStyle( props, { @@ -53,6 +52,7 @@ export function BaseSkeleton({ }, [show, isGroupLoading]); return showSkeleton ? ( | undefined; +function getDefaultIntl() { + if (!defaultIntl) { + defaultIntl = createIntl({ + locale: '', + }); + } + return defaultIntl; +} export function HyperlinkText({ translationId, @@ -70,7 +76,7 @@ export function HyperlinkText({ [basicTextProps.fontSize, basicTextProps.size], ); - const theIntl = scoped ? defaultIntl : intl; + const theIntl = scoped ? getDefaultIntl() : intl; const text = useMemo( () => diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx index 7a34636a34ad..a9b96dd1e499 100644 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx +++ b/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx @@ -12,6 +12,8 @@ import type { } from './openOAuthPopupTypes'; import type { SupabaseClient } from '@supabase/supabase-js'; +const ONEKEY_OAUTH_STATE_KEY = 'onekey_oauth_state'; + /** * Get OAuth redirect URL for web platform * @@ -66,17 +68,35 @@ export async function openOAuthPopupWeb(options: { persistSession?: boolean; }): Promise { const { authUrl, client, handleSessionPersistence, persistSession } = options; + return new Promise((resolve, reject) => { let settled = false; let inFlight = false; let pollIntervalId: ReturnType | null = null; let timeoutId: ReturnType | null = null; let expectedState: string | null = null; + let expectedOneKeyState: string | null = null; try { - expectedState = new URL(authUrl).searchParams.get('state'); + // "https://xxx.supabase.co/auth/v1/authorize?provider=google&redirect_to=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback&code_challenge=xxxx-xxx&code_challenge_method=s256&prompt=select_account" + const authUrlObj = new URL(authUrl); + expectedState = authUrlObj.searchParams.get('state'); + + // Parse our own state from the embedded redirect_to URL (defense-in-depth) + const redirectTo = authUrlObj.searchParams.get('redirect_to'); + if (redirectTo) { + try { + const redirectUrl = new URL(redirectTo); + expectedOneKeyState = redirectUrl.searchParams.get( + ONEKEY_OAUTH_STATE_KEY, + ); + } catch { + expectedOneKeyState = null; + } + } } catch { expectedState = null; + expectedOneKeyState = null; } const cleanup = (popup: Window | null) => { @@ -197,6 +217,22 @@ export async function openOAuthPopupWeb(options: { const url = new URL(popupUrl); const code = url.searchParams.get('code'); const state = url.searchParams.get('state'); + const oneKeyState = url.searchParams.get(ONEKEY_OAUTH_STATE_KEY); + + // Validate OneKey state (defense-in-depth). + if (expectedOneKeyState) { + if (!oneKeyState) { + rejectOnce( + new OneKeyLocalError('OAuth state is missing'), + popup, + ); + return; + } + if (oneKeyState !== expectedOneKeyState) { + rejectOnce(new OneKeyLocalError('OAuth state mismatch'), popup); + return; + } + } // Validate state (anti-CSRF / anti-injection). Supabase OAuth URLs should include `state=...` // and the redirect callback should echo it back. @@ -215,12 +251,16 @@ export async function openOAuthPopupWeb(options: { } if (code) { + // const _sessionBefore = await client.auth.getSession(); + // Exchange authorization code for session tokens using PKCE // The Supabase client automatically uses the stored code_verifier const { data, error } = await client.auth.exchangeCodeForSession( code, ); + // const _sessionAfter = await client.auth.getSession(); + if (error) { rejectOnce(new OneKeyLocalError(error.message), popup); return; diff --git a/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx b/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx index f7c7646d00fc..26230b9afd61 100644 --- a/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx +++ b/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx @@ -104,6 +104,8 @@ export function useSupabaseAuth() { const { persistSession } = options ?? {}; const clientTemp: SupabaseClient = createTemporarySupabaseClient(); + const ONEKEY_OAUTH_STATE_KEY = 'onekey_oauth_state'; + // For extension with CHROME_IDENTITY_API or CHROME_GET_AUTH_TOKEN methods, // we don't need Supabase OAuth URL - these methods build their own Google OAuth URL // and use signInWithIdToken instead @@ -149,6 +151,36 @@ export function useSupabaseAuth() { redirectTo = getOAuthRedirectUrlWeb(); } + // Defense-in-depth: Supabase PKCE URL may not include `state`. We embed our own + // nonce into redirectTo so the callback must carry it back to us. + if ( + redirectTo && + !platformEnv.isNative && + !platformEnv.isDesktop && + !platformEnv.isExtension + ) { + try { + const redirectUrl = new URL(redirectTo); + if (!redirectUrl.searchParams.has(ONEKEY_OAUTH_STATE_KEY)) { + // Prefer crypto-grade random on web; if unavailable, skip rather than generating weak state. + const bytes = new Uint8Array(16); + const cryptoObj = globalThis.crypto as + | undefined + | { getRandomValues: (arr: Uint8Array) => Uint8Array }; + if (cryptoObj?.getRandomValues) { + cryptoObj.getRandomValues(bytes); + const state = Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + redirectUrl.searchParams.set(ONEKEY_OAUTH_STATE_KEY, state); + redirectTo = redirectUrl.toString(); + } + } + } catch { + // ignore + } + } + const oauthUrlResult = await clientTemp.auth.signInWithOAuth({ provider, options: { diff --git a/packages/kit/src/provider/Container/index.tsx b/packages/kit/src/provider/Container/index.tsx index 49158680d886..da60d93fa90c 100644 --- a/packages/kit/src/provider/Container/index.tsx +++ b/packages/kit/src/provider/Container/index.tsx @@ -56,6 +56,7 @@ function DetailRouter() { + @@ -67,7 +68,6 @@ function DetailRouter() { - diff --git a/packages/kit/src/views/ReferFriends/pages/ReferAFriend/components/ReferAFriendIntroPhase/index.tsx b/packages/kit/src/views/ReferFriends/pages/ReferAFriend/components/ReferAFriendIntroPhase/index.tsx index 7a40b6bfc47b..11988a78b126 100644 --- a/packages/kit/src/views/ReferFriends/pages/ReferAFriend/components/ReferAFriendIntroPhase/index.tsx +++ b/packages/kit/src/views/ReferFriends/pages/ReferAFriend/components/ReferAFriendIntroPhase/index.tsx @@ -58,7 +58,8 @@ export function ReferAFriendIntroPhase({ { amount: ( From 807d421d6b8614bd5d0b57746d5753ebbe7afbbb Mon Sep 17 00:00:00 2001 From: morizon Date: Sat, 20 Dec 2025 17:56:35 +0800 Subject: [PATCH 05/66] feat: enhance OAuth flow with oneKeyState handling and refactor related components - Added support for `oneKeyState` in the OAuth callback handling to improve security and state validation. - Updated `openOAuthPopupDesktopLocalhost` to manage OAuth sessions effectively, including session persistence options. - Refactored `useSupabaseAuth` to streamline OAuth processes and ensure compatibility with the new state handling. - Removed the deprecated `openOAuthPopupDesktopLocalhost` function to clean up the codebase. - Enhanced error handling across various OAuth methods to provide clearer feedback on authentication issues. --- .../oauthLocalServer/oauthCallbackHtml.ts | 3 +- .../oauthLocalServer/oauthLocalServer.ts | 4 +- .../src/primitives/Skeleton/BaseSkeleton.tsx | 16 +- .../src/primitives/Skeleton/index.tsx | 4 +- .../OneKeyAuth/openOAuthPopupDesktop.tsx | 310 +++++++++++++++++- .../openOAuthPopupDesktopLocalhost.ts | 259 --------------- .../OneKeyAuth/openOAuthPopupExt.tsx | 33 +- .../OneKeyAuth/openOAuthPopupWeb.tsx | 135 ++++---- .../OneKeyAuth/supabase/useSupabaseAuth.tsx | 13 +- packages/shared/src/consts/authConsts.ts | 4 + 10 files changed, 405 insertions(+), 376 deletions(-) delete mode 100644 packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktopLocalhost.ts diff --git a/apps/desktop/app/service/oauthLocalServer/oauthCallbackHtml.ts b/apps/desktop/app/service/oauthLocalServer/oauthCallbackHtml.ts index df25ae795ce5..d0ce2fdfe70e 100644 --- a/apps/desktop/app/service/oauthLocalServer/oauthCallbackHtml.ts +++ b/apps/desktop/app/service/oauthLocalServer/oauthCallbackHtml.ts @@ -74,6 +74,7 @@ export const OAUTH_CALLBACK_SUCCESS_HTML = ` const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get('code'); const state = urlParams.get('state'); + const oneKeyState = urlParams.get('onekey_oauth_state'); // Clear URL query ASAP to avoid leaking code in the address bar. try { history.replaceState(null, document.title, window.location.pathname); @@ -84,7 +85,7 @@ export const OAUTH_CALLBACK_SUCCESS_HTML = ` fetch('/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ code, state }), + body: JSON.stringify({ code, state, oneKeyState }), }).then(() => { setTimeout(tryClose, 1500); }).catch(() => { diff --git a/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts b/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts index 10f4467001a3..f85ff2485781 100644 --- a/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts +++ b/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts @@ -227,9 +227,10 @@ export async function startOAuthServer(): Promise<{ port: number }> { }); req.on('end', () => { try { - const { code, state } = JSON.parse(body) as { + const { code, state, oneKeyState } = JSON.parse(body) as { code: string; state?: string; + oneKeyState?: string; }; if (code && mainWindow && !mainWindow.isDestroyed()) { @@ -237,6 +238,7 @@ export async function startOAuthServer(): Promise<{ port: number }> { mainWindow.webContents.send(OAUTH_CALLBACK_DESKTOP_CHANNEL, { code, state, + oneKeyState, }); } diff --git a/packages/components/src/primitives/Skeleton/BaseSkeleton.tsx b/packages/components/src/primitives/Skeleton/BaseSkeleton.tsx index 56f6d1547bc0..d7e00f8037a0 100644 --- a/packages/components/src/primitives/Skeleton/BaseSkeleton.tsx +++ b/packages/components/src/primitives/Skeleton/BaseSkeleton.tsx @@ -1,5 +1,5 @@ -import { useMemo } from 'react'; -import type { ComponentRef, ForwardedRef } from 'react'; +import { forwardRef, useMemo } from 'react'; +import type { ComponentRef } from 'react'; import type { StackStyle } from '@onekeyhq/components/src/shared/tamagui'; import { @@ -14,10 +14,10 @@ import { useIsGroupLoading } from './context'; import type { ISkeletonProps } from './type'; -export function BaseSkeleton( - { colorMode, children, show, ...props }: ISkeletonProps, - ref: ForwardedRef>, -) { +export const BaseSkeleton = forwardRef< + ComponentRef, + ISkeletonProps +>(({ colorMode, children, show, ...props }: ISkeletonProps, ref) => { const [{ className: classNameProp, ...restProps }, style] = usePropsAndStyle( props, { @@ -65,4 +65,6 @@ export function BaseSkeleton( ) : ( children || null ); -} +}); + +BaseSkeleton.displayName = 'BaseSkeleton'; diff --git a/packages/components/src/primitives/Skeleton/index.tsx b/packages/components/src/primitives/Skeleton/index.tsx index ad3cb2b8b53b..063952d1c343 100644 --- a/packages/components/src/primitives/Skeleton/index.tsx +++ b/packages/components/src/primitives/Skeleton/index.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useMemo } from 'react'; +import { useMemo } from 'react'; import { styled, @@ -124,7 +124,7 @@ function SkeletonGroup({ } export const Skeleton = withStaticProperties( - styled(forwardRef(BaseSkeleton), { + styled(BaseSkeleton, { name: 'Skeleton', } as const), { diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx index 9ecb22f8d667..3f069ec6a9e6 100644 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx +++ b/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx @@ -1,25 +1,317 @@ +import { Dialog } from '@onekeyhq/components'; +import type { IDialogInstance } from '@onekeyhq/components'; import type { IDesktopOpenUrlEventData } from '@onekeyhq/desktop/app/app'; import { ipcMessageKeys } from '@onekeyhq/desktop/app/config'; import { EDesktopOAuthMethod, + OAUTH_CALLBACK_DESKTOP_CHANNEL, OAUTH_DESKTOP_WEBVIEW_HEIGHT, OAUTH_DESKTOP_WEBVIEW_WIDTH, OAUTH_FLOW_TIMEOUT_MS, OAUTH_TOKEN_KEY_ACCESS_TOKEN, OAUTH_TOKEN_KEY_REFRESH_TOKEN, + ONEKEY_OAUTH_STATE_KEY, } from '@onekeyhq/shared/src/consts/authConsts'; import { ONEKEY_APP_DEEP_LINK } from '@onekeyhq/shared/src/consts/deeplinkConsts'; import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; +import platformEnv from '@onekeyhq/shared/src/platformEnv'; import type { IHandleOAuthSessionPersistenceParams, IOAuthPopupResult, } from './openOAuthPopupTypes'; +import type { SupabaseClient } from '@supabase/supabase-js'; // ============================================================================ // Desktop OAuth Methods // ============================================================================ +/** + * OAuth helper for Desktop (Electron) platform using localhost HTTP server + * with Supabase OAuth redirecting back to localhost (fixed port range). + * + * This method uses Supabase as the OAuth intermediary with PKCE flow: + * Google -> Supabase -> localhost callback (authorization code in URL query) + * + * How it works: + * 1. Main process starts a localhost HTTP server on a fixed port range + * 2. Renderer opens Supabase OAuth URL in system browser (skipBrowserRedirect=true) + * 3. Supabase handles Google OAuth and redirects back with authorization code + * 4. Main process extracts code from URL query and sends it to renderer via IPC + * 5. Renderer exchanges code for session tokens using Supabase client + * 6. Renderer persists session via handleSessionPersistence + * + * Supabase Configuration: + * - Add Redirect URLs (fixed port range) + * + * @param options - Configuration options + * @param options.authUrl - Supabase OAuth URL (skipBrowserRedirect=true) + * @param options.client - Supabase client instance for exchanging code + * @param options.handleSessionPersistence - Function to handle session persistence + * @param options.persistSession - Whether to persist the session + * @returns Promise with success status and session tokens + */ +export async function openOAuthPopupDesktopLocalhost(options: { + authUrl: string; + client: SupabaseClient; + handleSessionPersistence: ( + params: IHandleOAuthSessionPersistenceParams, + ) => Promise; + persistSession?: boolean; +}): Promise { + const { authUrl, client, handleSessionPersistence, persistSession } = options; + + // Check if desktopApiProxy is available + if (!platformEnv.isDesktop || !globalThis.desktopApiProxy?.oauthLocalServer) { + throw new OneKeyLocalError( + 'Desktop OAuth Local Server API is not available', + ); + } + + return new Promise((resolve, reject) => { + void (async () => { + try { + let settled = false; + let timeoutId: ReturnType | null = null; + let dialogClosed = false; + let waitingDialog: IDialogInstance | null = null; + let expectedState: string | null = null; + let expectedOneKeyState: string | null = null; + + try { + const authUrlObj = new URL(authUrl); + expectedState = authUrlObj.searchParams.get('state'); + + const redirectTo = authUrlObj.searchParams.get('redirect_to'); + if (redirectTo) { + try { + expectedOneKeyState = new URL(redirectTo).searchParams.get( + ONEKEY_OAUTH_STATE_KEY, + ); + } catch { + expectedOneKeyState = null; + } + } + } catch { + expectedState = null; + expectedOneKeyState = null; + } + + const cleanupFn = { + cleanup: async () => {}, + }; + + // Listen for callback with authorization code via IPC (PKCE flow) + const handleCallback = async ( + _event: Electron.IpcRendererEvent, + data: { + code?: string; + state?: string; + oneKeyState?: string; + }, + ) => { + if (settled) { + return; + } + settled = true; + // Remove listener using desktopApi (for IPC events) + if (globalThis.desktopApi) { + globalThis.desktopApi.removeIpcEventListener( + OAUTH_CALLBACK_DESKTOP_CHANNEL, + handleCallback, + ); + } + + try { + dialogClosed = true; + await Promise.resolve(waitingDialog?.close()); + const code = data.code; + const state = data.state; + const oneKeyState = data.oneKeyState; + + if (!code) { + await cleanupFn.cleanup(); + reject(new OneKeyLocalError('Authorization code is missing')); + return; + } + + if (!expectedOneKeyState) { + await cleanupFn.cleanup(); + reject( + new OneKeyLocalError('Expected OneKey OAuth state is missing'), + ); + return; + } + + // Validate OneKey state (defense-in-depth). + if (!oneKeyState) { + await cleanupFn.cleanup(); + reject(new OneKeyLocalError('OAuth state is missing')); + return; + } + + if (oneKeyState !== expectedOneKeyState) { + await cleanupFn.cleanup(); + reject(new OneKeyLocalError('OAuth state mismatch')); + return; + } + + // Validate state (anti-CSRF / anti-injection). This does not change the redirect URI. + // Supabase OAuth URLs should include `state=...` and the redirect callback should echo it back. + if (expectedState) { + if (!state) { + await cleanupFn.cleanup(); + reject(new OneKeyLocalError('OAuth state is missing')); + return; + } + if (state !== expectedState) { + await cleanupFn.cleanup(); + reject(new OneKeyLocalError('OAuth state mismatch')); + return; + } + } + + // Exchange authorization code for session tokens using PKCE + // The Supabase client automatically uses the stored code_verifier + const { data: exchangeData, error } = + await client.auth.exchangeCodeForSession(code); + + if (error) { + await cleanupFn.cleanup(); + reject(new OneKeyLocalError(error.message)); + return; + } + + const session = exchangeData.session; + if (!session) { + await cleanupFn.cleanup(); + reject( + new OneKeyLocalError( + 'Failed to exchange authorization code for session', + ), + ); + return; + } + + const accessToken = session.access_token; + const refreshToken = session.refresh_token; + + // Handle session persistence + await handleSessionPersistence({ + accessToken, + refreshToken, + persistSession, + }); + + await cleanupFn.cleanup(); + resolve({ + success: true, + session: { accessToken, refreshToken }, + }); + } catch (error) { + await cleanupFn.cleanup(); + reject( + new OneKeyLocalError( + error instanceof Error ? error.message : 'OAuth failed', + ), + ); + } + }; + + cleanupFn.cleanup = async () => { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + if (globalThis.desktopApi) { + globalThis.desktopApi.removeIpcEventListener( + OAUTH_CALLBACK_DESKTOP_CHANNEL, + handleCallback, + ); + } + try { + await globalThis.desktopApiProxy.oauthLocalServer.stopServer(); + } catch { + // Ignore stop errors. + } + try { + if (!dialogClosed) { + await Promise.resolve(waitingDialog?.close()); + } + } catch { + // Ignore close errors. + } + }; + + // Show an in-app "waiting" dialog so users can cancel immediately. + // Note: When opening **external system browsers**, we cannot reliably detect + // whether the browser window/tab was closed. Cancel is the only reliable way. + waitingDialog = Dialog.show({ + title: 'Sign in', + description: + 'Complete sign-in in your browser, then return to OneKey.', + showFooter: true, + showConfirmButton: false, + showCancelButton: true, + onCancel: async (close) => { + if (settled) { + await close(); + return; + } + settled = true; + dialogClosed = true; + await close(); + await cleanupFn.cleanup(); + reject(new OneKeyLocalError('OAuth sign-in was cancelled')); + }, + onClose: async (extra) => { + // Treat manual dialog dismissal as cancel. + if (extra?.flag === 'cancel' && !settled) { + settled = true; + dialogClosed = true; + await cleanupFn.cleanup(); + reject(new OneKeyLocalError('OAuth sign-in was cancelled')); + } + }, + }); + + // Add listener using desktopApi (for IPC events) + if (globalThis.desktopApi) { + globalThis.desktopApi.addIpcEventListener( + OAUTH_CALLBACK_DESKTOP_CHANNEL, + handleCallback, + ); + } + + // Open Supabase OAuth in system browser + await globalThis.desktopApiProxy.oauthLocalServer.openBrowser(authUrl); + + // Timeout after 5 minutes + timeoutId = setTimeout(() => { + if (settled) { + return; + } + settled = true; + void cleanupFn.cleanup().finally(() => { + reject(new OneKeyLocalError('OAuth sign-in timed out')); + }); + }, OAUTH_FLOW_TIMEOUT_MS); + } catch (error) { + Dialog.debugMessage({ + title: 'OAuth', + debugMessage: + error instanceof Error ? error.message : 'OAuth setup failed', + }); + reject( + new OneKeyLocalError( + error instanceof Error ? error.message : 'OAuth setup failed', + ), + ); + } + })(); + }); +} + /** * Get OAuth redirect URL for desktop platform (Electron) * @@ -212,10 +504,11 @@ export function openOAuthPopupDesktopWebview(options: { }, }); } else { - resolve({ - success: false, - session: undefined, - }); + reject( + new OneKeyLocalError( + 'OAuth authentication failed: access token or refresh token is missing', + ), + ); } } catch (error) { reject(error); @@ -340,10 +633,11 @@ export function openOAuthPopupDesktopDeepLink(options: { }, }); } else { - resolve({ - success: false, - session: undefined, - }); + reject( + new OneKeyLocalError( + 'OAuth authentication failed: access token or refresh token is missing', + ), + ); } } catch (error) { reject(error); diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktopLocalhost.ts b/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktopLocalhost.ts deleted file mode 100644 index aadd110e325f..000000000000 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktopLocalhost.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { Dialog } from '@onekeyhq/components'; -import type { IDialogInstance } from '@onekeyhq/components'; -import { - OAUTH_CALLBACK_DESKTOP_CHANNEL, - OAUTH_FLOW_TIMEOUT_MS, -} from '@onekeyhq/shared/src/consts/authConsts'; -import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; -import platformEnv from '@onekeyhq/shared/src/platformEnv'; - -import type { - IHandleOAuthSessionPersistenceParams, - IOAuthPopupResult, -} from './openOAuthPopupTypes'; -import type { SupabaseClient } from '@supabase/supabase-js'; - -/** - * OAuth helper for Desktop (Electron) platform using localhost HTTP server - * with Supabase OAuth redirecting back to localhost (fixed port range). - * - * This method uses Supabase as the OAuth intermediary with PKCE flow: - * Google -> Supabase -> localhost callback (authorization code in URL query) - * - * How it works: - * 1. Main process starts a localhost HTTP server on a fixed port range - * 2. Renderer opens Supabase OAuth URL in system browser (skipBrowserRedirect=true) - * 3. Supabase handles Google OAuth and redirects back with authorization code - * 4. Main process extracts code from URL query and sends it to renderer via IPC - * 5. Renderer exchanges code for session tokens using Supabase client - * 6. Renderer persists session via handleSessionPersistence - * - * Supabase Configuration: - * - Add Redirect URLs (fixed port range) - * - * @param options - Configuration options - * @param options.authUrl - Supabase OAuth URL (skipBrowserRedirect=true) - * @param options.client - Supabase client instance for exchanging code - * @param options.handleSessionPersistence - Function to handle session persistence - * @param options.persistSession - Whether to persist the session - * @returns Promise with success status and session tokens - */ -export async function openOAuthPopupDesktopLocalhost(options: { - authUrl: string; - client: SupabaseClient; - handleSessionPersistence: ( - params: IHandleOAuthSessionPersistenceParams, - ) => Promise; - persistSession?: boolean; -}): Promise { - const { authUrl, client, handleSessionPersistence, persistSession } = options; - - // Check if desktopApiProxy is available - if (!platformEnv.isDesktop || !globalThis.desktopApiProxy?.oauthLocalServer) { - throw new OneKeyLocalError( - 'Desktop OAuth Local Server API is not available', - ); - } - - return new Promise((resolve, reject) => { - void (async () => { - try { - let settled = false; - let timeoutId: ReturnType | null = null; - let dialogClosed = false; - let waitingDialog: IDialogInstance | null = null; - let expectedState: string | null = null; - - try { - expectedState = new URL(authUrl).searchParams.get('state'); - } catch { - expectedState = null; - } - - const cleanupFn = { - cleanup: async () => {}, - }; - - // Listen for callback with authorization code via IPC (PKCE flow) - const handleCallback = async ( - _event: Electron.IpcRendererEvent, - data: { - code?: string; - state?: string; - }, - ) => { - if (settled) { - return; - } - settled = true; - // Remove listener using desktopApi (for IPC events) - if (globalThis.desktopApi) { - globalThis.desktopApi.removeIpcEventListener( - OAUTH_CALLBACK_DESKTOP_CHANNEL, - handleCallback, - ); - } - - try { - dialogClosed = true; - await Promise.resolve(waitingDialog?.close()); - const code = data.code; - const state = data.state; - - if (!code) { - await cleanupFn.cleanup(); - resolve({ success: false, session: undefined }); - return; - } - - // Validate state (anti-CSRF / anti-injection). This does not change the redirect URI. - // Supabase OAuth URLs should include `state=...` and the redirect callback should echo it back. - if (expectedState) { - if (!state) { - await cleanupFn.cleanup(); - reject(new OneKeyLocalError('OAuth state is missing')); - return; - } - if (state !== expectedState) { - await cleanupFn.cleanup(); - reject(new OneKeyLocalError('OAuth state mismatch')); - return; - } - } - - // Exchange authorization code for session tokens using PKCE - // The Supabase client automatically uses the stored code_verifier - const { data: exchangeData, error } = - await client.auth.exchangeCodeForSession(code); - - if (error) { - await cleanupFn.cleanup(); - reject(new OneKeyLocalError(error.message)); - return; - } - - const session = exchangeData.session; - if (!session) { - await cleanupFn.cleanup(); - resolve({ success: false, session: undefined }); - return; - } - - const accessToken = session.access_token; - const refreshToken = session.refresh_token; - - // Handle session persistence - await handleSessionPersistence({ - accessToken, - refreshToken, - persistSession, - }); - - await cleanupFn.cleanup(); - resolve({ - success: true, - session: { accessToken, refreshToken }, - }); - } catch (error) { - await cleanupFn.cleanup(); - reject( - new OneKeyLocalError( - error instanceof Error ? error.message : 'OAuth failed', - ), - ); - } - }; - - cleanupFn.cleanup = async () => { - if (timeoutId) { - clearTimeout(timeoutId); - timeoutId = null; - } - if (globalThis.desktopApi) { - globalThis.desktopApi.removeIpcEventListener( - OAUTH_CALLBACK_DESKTOP_CHANNEL, - handleCallback, - ); - } - try { - await globalThis.desktopApiProxy.oauthLocalServer.stopServer(); - } catch { - // Ignore stop errors. - } - try { - if (!dialogClosed) { - await Promise.resolve(waitingDialog?.close()); - } - } catch { - // Ignore close errors. - } - }; - - // Show an in-app "waiting" dialog so users can cancel immediately. - // Note: When opening **external system browsers**, we cannot reliably detect - // whether the browser window/tab was closed. Cancel is the only reliable way. - waitingDialog = Dialog.show({ - title: 'Sign in', - description: - 'Complete sign-in in your browser, then return to OneKey.', - showFooter: true, - showConfirmButton: false, - showCancelButton: true, - onCancel: async (close) => { - if (settled) { - await close(); - return; - } - settled = true; - dialogClosed = true; - await close(); - await cleanupFn.cleanup(); - reject(new OneKeyLocalError('OAuth sign-in was cancelled')); - }, - onClose: async (extra) => { - // Treat manual dialog dismissal as cancel. - if (extra?.flag === 'cancel' && !settled) { - settled = true; - dialogClosed = true; - await cleanupFn.cleanup(); - reject(new OneKeyLocalError('OAuth sign-in was cancelled')); - } - }, - }); - - // Add listener using desktopApi (for IPC events) - if (globalThis.desktopApi) { - globalThis.desktopApi.addIpcEventListener( - OAUTH_CALLBACK_DESKTOP_CHANNEL, - handleCallback, - ); - } - - // Open Supabase OAuth in system browser - await globalThis.desktopApiProxy.oauthLocalServer.openBrowser(authUrl); - - // Timeout after 5 minutes - timeoutId = setTimeout(() => { - if (settled) { - return; - } - settled = true; - void cleanupFn.cleanup().finally(() => { - reject(new OneKeyLocalError('OAuth sign-in timed out')); - }); - }, OAUTH_FLOW_TIMEOUT_MS); - } catch (error) { - Dialog.debugMessage({ - title: 'OAuth', - debugMessage: - error instanceof Error ? error.message : 'OAuth setup failed', - }); - reject( - new OneKeyLocalError( - error instanceof Error ? error.message : 'OAuth setup failed', - ), - ); - } - })(); - }); -} diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx index 5e85213809cb..660542fb9f3e 100644 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx +++ b/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx @@ -210,10 +210,7 @@ export async function openOAuthPopupExtIdentity(options: { } } if (!code) { - return { - success: false, - session: undefined, - }; + throw new OneKeyLocalError('Authorization code is missing'); } const { data, error: exchangeError } = @@ -223,10 +220,9 @@ export async function openOAuthPopupExtIdentity(options: { } if (!data.session) { - return { - success: false, - session: undefined, - }; + throw new OneKeyLocalError( + 'Failed to exchange authorization code for session', + ); } const accessToken = data.session.access_token; @@ -259,10 +255,9 @@ export async function openOAuthPopupExtIdentity(options: { const callbackUrl = await launchWebAuthFlowWithTimeout(authUrl); if (!callbackUrl) { - return { - success: false, - session: undefined, - }; + throw new OneKeyLocalError( + 'OAuth authentication failed: callback URL is missing', + ); } return signIn({ callbackUrl, signInParams }); @@ -333,10 +328,7 @@ export async function openOAuthPopupExtIdentity(options: { } if (!data.session) { - return { - success: false, - session: undefined, - }; + throw new OneKeyLocalError('Failed to exchange ID token for session'); } const accessToken = data.session.access_token; @@ -741,10 +733,11 @@ export function openOAuthPopupExtWindow(options: { }); }); } else { - resolve({ - success: false, - session: undefined, - }); + reject( + new OneKeyLocalError( + 'OAuth authentication failed: access token or refresh token is missing', + ), + ); } } catch (error) { reject( diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx index a9b96dd1e499..fad64ade69d5 100644 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx +++ b/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx @@ -195,11 +195,10 @@ export async function openOAuthPopupWeb(options: { popup, ); } else { - resolveOnce( - { - success: false, - session: undefined, - }, + rejectOnce( + new OneKeyLocalError( + 'OAuth authentication failed: no session found after popup closed', + ), popup, ); } @@ -219,19 +218,31 @@ export async function openOAuthPopupWeb(options: { const state = url.searchParams.get('state'); const oneKeyState = url.searchParams.get(ONEKEY_OAUTH_STATE_KEY); + if (!code) { + rejectOnce( + new OneKeyLocalError('Authorization code is missing'), + popup, + ); + return; + } + + if (!expectedOneKeyState) { + rejectOnce( + new OneKeyLocalError('Expected OneKey OAuth state is missing'), + popup, + ); + return; + } + // Validate OneKey state (defense-in-depth). - if (expectedOneKeyState) { - if (!oneKeyState) { - rejectOnce( - new OneKeyLocalError('OAuth state is missing'), - popup, - ); - return; - } - if (oneKeyState !== expectedOneKeyState) { - rejectOnce(new OneKeyLocalError('OAuth state mismatch'), popup); - return; - } + if (!oneKeyState) { + rejectOnce(new OneKeyLocalError('OAuth state is missing'), popup); + return; + } + + if (oneKeyState !== expectedOneKeyState) { + rejectOnce(new OneKeyLocalError('OAuth state mismatch'), popup); + return; } // Validate state (anti-CSRF / anti-injection). Supabase OAuth URLs should include `state=...` @@ -250,62 +261,48 @@ export async function openOAuthPopupWeb(options: { } } - if (code) { - // const _sessionBefore = await client.auth.getSession(); + // Exchange authorization code for session tokens using PKCE + // The Supabase client automatically uses the stored code_verifier + const { data, error } = await client.auth.exchangeCodeForSession( + code, + ); - // Exchange authorization code for session tokens using PKCE - // The Supabase client automatically uses the stored code_verifier - const { data, error } = await client.auth.exchangeCodeForSession( - code, - ); + if (error) { + rejectOnce(new OneKeyLocalError(error.message), popup); + return; + } - // const _sessionAfter = await client.auth.getSession(); + const session = data.session; + if (!session) { + rejectOnce( + new OneKeyLocalError( + 'Failed to exchange authorization code for session', + ), + popup, + ); + return; + } - if (error) { - rejectOnce(new OneKeyLocalError(error.message), popup); - return; - } + const accessToken = session.access_token; + const refreshToken = session.refresh_token; - const session = data.session; - if (session) { - const accessToken = session.access_token; - const refreshToken = session.refresh_token; + await handleSessionPersistence({ + accessToken, + refreshToken, + persistSession, + loginToPrime: false, // openOAuthPopupWeb doesn't handle Prime login + }); - await handleSessionPersistence({ + resolveOnce( + { + success: true, + session: { accessToken, refreshToken, - persistSession, - loginToPrime: false, // openOAuthPopupWeb doesn't handle Prime login - }); - - resolveOnce( - { - success: true, - session: { - accessToken, - refreshToken, - }, - }, - popup, - ); - } else { - resolveOnce( - { - success: false, - session: undefined, - }, - popup, - ); - } - } else { - resolveOnce( - { - success: false, - session: undefined, }, - popup, - ); - } + }, + popup, + ); } } catch { // Cross-origin error - popup is on different domain, continue polling @@ -319,13 +316,7 @@ export async function openOAuthPopupWeb(options: { // Cleanup after timeout (5 minutes) timeoutId = setTimeout(() => { - resolveOnce( - { - success: false, - session: undefined, - }, - popup, - ); + rejectOnce(new OneKeyLocalError('OAuth sign-in timed out'), popup); }, OAUTH_FLOW_TIMEOUT_MS); }); } diff --git a/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx b/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx index 26230b9afd61..e9d174cd5ba3 100644 --- a/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx +++ b/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx @@ -10,6 +10,7 @@ import { EDesktopOAuthMethod, EExtensionOAuthMethod, GOOGLE_CHROME_EXTENSION_CLIENT_ID, + ONEKEY_OAUTH_STATE_KEY, } from '@onekeyhq/shared/src/consts/authConsts'; import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; import { ETranslations } from '@onekeyhq/shared/src/locale'; @@ -18,9 +19,9 @@ import platformEnv from '@onekeyhq/shared/src/platformEnv'; import { getOAuthRedirectUrlDesktop, openOAuthPopupDesktopDeepLink, + openOAuthPopupDesktopLocalhost, openOAuthPopupDesktopWebview, } from '../openOAuthPopupDesktop'; -import { openOAuthPopupDesktopLocalhost } from '../openOAuthPopupDesktopLocalhost'; import { getOAuthRedirectUrlExt, openOAuthPopupExtIdToken, @@ -104,8 +105,6 @@ export function useSupabaseAuth() { const { persistSession } = options ?? {}; const clientTemp: SupabaseClient = createTemporarySupabaseClient(); - const ONEKEY_OAUTH_STATE_KEY = 'onekey_oauth_state'; - // For extension with CHROME_IDENTITY_API or CHROME_GET_AUTH_TOKEN methods, // we don't need Supabase OAuth URL - these methods build their own Google OAuth URL // and use signInWithIdToken instead @@ -155,9 +154,11 @@ export function useSupabaseAuth() { // nonce into redirectTo so the callback must carry it back to us. if ( redirectTo && - !platformEnv.isNative && - !platformEnv.isDesktop && - !platformEnv.isExtension + (platformEnv.isWeb || + // Desktop localhost server method + (platformEnv.isDesktop && + DEFAULT_DESKTOP_OAUTH_METHOD === + EDesktopOAuthMethod.LOCALHOST_SERVER)) ) { try { const redirectUrl = new URL(redirectTo); diff --git a/packages/shared/src/consts/authConsts.ts b/packages/shared/src/consts/authConsts.ts index 138c447a40de..e5ab264f10ee 100644 --- a/packages/shared/src/consts/authConsts.ts +++ b/packages/shared/src/consts/authConsts.ts @@ -63,6 +63,10 @@ export const OAUTH_DESKTOP_WEBVIEW_HEIGHT = 640; // Poll / focus interval used by web popup + extension OAuth window focusing export const OAUTH_POLL_INTERVAL_MS = 500; +// OneKey-owned state (defense-in-depth) used for OAuth flows where the upstream provider +// does not reliably include `state` in the authorize URL / callback. +export const ONEKEY_OAUTH_STATE_KEY = 'onekey_oauth_state'; + // Common OAuth callback token keys (hash/search params) export const OAUTH_TOKEN_KEY_ACCESS_TOKEN = 'access_token'; export const OAUTH_TOKEN_KEY_REFRESH_TOKEN = 'refresh_token'; From abad74ceb24105610e6e438a964f120505b70e1c Mon Sep 17 00:00:00 2001 From: morizon Date: Sat, 20 Dec 2025 19:40:10 +0800 Subject: [PATCH 06/66] refactor: update OAuth callback paths and enhance state handling - Changed OAuth callback paths for desktop and web to `/oauth_callback_desktop` and `/oauth_callback_web`, respectively, improving clarity and consistency. - Introduced `ensureOneKeyOAuthState` utility to guarantee the presence of the `ONEKEY_OAUTH_STATE_KEY` parameter in redirect URLs, enhancing security. - Updated various components to utilize the new callback paths and state handling, ensuring robust OAuth flow and validation. - Improved error handling and state validation in the OAuth process to mitigate potential security risks. --- .../oauthLocalServer/oauthCallbackHtml.ts | 4 +- .../oauthLocalServer/oauthLocalServer.ts | 23 ++++---- .../src/components/OneKeyAuth/oauthUtils.ts | 43 +++++++++++++++ .../OneKeyAuth/openOAuthPopupDesktop.tsx | 4 +- .../OneKeyAuth/openOAuthPopupExt.tsx | 53 ++++++++++++++++--- .../OneKeyAuth/openOAuthPopupWeb.tsx | 8 +-- .../OneKeyAuth/supabase/useSupabaseAuth.tsx | 23 +------- packages/shared/src/consts/authConsts.ts | 10 ++++ 8 files changed, 124 insertions(+), 44 deletions(-) create mode 100644 packages/kit/src/components/OneKeyAuth/oauthUtils.ts diff --git a/apps/desktop/app/service/oauthLocalServer/oauthCallbackHtml.ts b/apps/desktop/app/service/oauthLocalServer/oauthCallbackHtml.ts index d0ce2fdfe70e..87048724882e 100644 --- a/apps/desktop/app/service/oauthLocalServer/oauthCallbackHtml.ts +++ b/apps/desktop/app/service/oauthLocalServer/oauthCallbackHtml.ts @@ -1,8 +1,8 @@ /** - * HTML templates returned by the localhost OAuth callback server (`/callback`). + * HTML templates returned by the localhost OAuth callback server (`/oauth_callback_desktop`). * * Why this exists: - * - Supabase redirects back to `http://127.0.0.1:/callback` with `code` (and `state`) + * - Supabase redirects back to `http://127.0.0.1:/oauth_callback_desktop` with `code` (and `state`) * in the URL query string (PKCE authorization code flow). * - We return an HTML page with JS to extract `code`/`state`, then POST them to `/complete` * so the desktop app can validate state (anti-CSRF) and exchange the code for a session. diff --git a/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts b/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts index f85ff2485781..28b3a5614e90 100644 --- a/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts +++ b/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts @@ -5,6 +5,7 @@ import { app, shell } from 'electron'; import { OAUTH_CALLBACK_DESKTOP_CHANNEL, + OAUTH_CALLBACK_DESKTOP_PATH, OAUTH_POPUP_HEIGHT, OAUTH_POPUP_WIDTH, } from '@onekeyhq/shared/src/consts/authConsts'; @@ -162,16 +163,16 @@ function tryOpenChromeAppWindow(url: string): boolean { // Fixed port range for OAuth callback // Web Application type requires explicit port configuration in Google Cloud Console // These ports must be added to Authorized redirect URIs: -// http://localhost:19185/callback -// http://localhost:19285/callback -// http://localhost:19385/callback -// http://localhost:19485/callback -// http://localhost:19585/callback -// http://127.0.0.1:19185/callback -// http://127.0.0.1:19285/callback -// http://127.0.0.1:19385/callback -// http://127.0.0.1:19485/callback -// http://127.0.0.1:19585/callback +// http://localhost:19185/oauth_callback_desktop +// http://localhost:19285/oauth_callback_desktop +// http://localhost:19385/oauth_callback_desktop +// http://localhost:19485/oauth_callback_desktop +// http://localhost:19585/oauth_callback_desktop +// http://127.0.0.1:19185/oauth_callback_desktop +// http://127.0.0.1:19285/oauth_callback_desktop +// http://127.0.0.1:19385/oauth_callback_desktop +// http://127.0.0.1:19485/oauth_callback_desktop +// http://127.0.0.1:19585/oauth_callback_desktop const OAUTH_PORTS = [ 19_185, 19_285, 19_385, 19_485, 19_585, // @@ -201,7 +202,7 @@ export async function startOAuthServer(): Promise<{ port: number }> { const url = new URL(req.url || '/', 'http://localhost'); // Handle callback from OAuth (Supabase redirects back to localhost with authorization code in URL query) - if (url.pathname === '/callback') { + if (url.pathname === OAUTH_CALLBACK_DESKTOP_PATH) { // PKCE flow: authorization code is in URL query string (not hash) const error = url.searchParams.get('error'); diff --git a/packages/kit/src/components/OneKeyAuth/oauthUtils.ts b/packages/kit/src/components/OneKeyAuth/oauthUtils.ts new file mode 100644 index 000000000000..362337754be6 --- /dev/null +++ b/packages/kit/src/components/OneKeyAuth/oauthUtils.ts @@ -0,0 +1,43 @@ +import { ONEKEY_OAUTH_STATE_KEY } from '@onekeyhq/shared/src/consts/authConsts'; + +/** + * Ensures that the redirectTo URL contains ONEKEY_OAUTH_STATE_KEY parameter. + * If the parameter is missing, generates a cryptographically secure random state + * and appends it to the URL. + * + * Defense-in-depth: Supabase PKCE URL may not include `state`. We embed our own + * nonce into redirectTo so the callback must carry it back to us. + * + * @param redirectTo - The redirect URL to ensure state parameter exists + * @returns The redirect URL with ONEKEY_OAUTH_STATE_KEY parameter guaranteed to exist, or original URL if crypto is unavailable + */ +export function ensureOneKeyOAuthState( + redirectTo: string | undefined, +): string | undefined { + if (!redirectTo) { + return redirectTo; + } + + try { + const redirectUrl = new URL(redirectTo); + if (!redirectUrl.searchParams.has(ONEKEY_OAUTH_STATE_KEY)) { + // Prefer crypto-grade random; if unavailable, skip rather than generating weak state. + const bytes = new Uint8Array(16); + const cryptoObj = globalThis.crypto as + | undefined + | { getRandomValues: (arr: Uint8Array) => Uint8Array }; + if (cryptoObj?.getRandomValues) { + cryptoObj.getRandomValues(bytes); + const state = Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + redirectUrl.searchParams.set(ONEKEY_OAUTH_STATE_KEY, state); + return redirectUrl.toString(); + } + } + return redirectTo; + } catch { + // If URL parsing fails, return original URL + return redirectTo; + } +} diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx index 3f069ec6a9e6..758102632884 100644 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx +++ b/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx @@ -5,6 +5,7 @@ import { ipcMessageKeys } from '@onekeyhq/desktop/app/config'; import { EDesktopOAuthMethod, OAUTH_CALLBACK_DESKTOP_CHANNEL, + OAUTH_CALLBACK_DESKTOP_PATH, OAUTH_DESKTOP_WEBVIEW_HEIGHT, OAUTH_DESKTOP_WEBVIEW_WIDTH, OAUTH_FLOW_TIMEOUT_MS, @@ -348,7 +349,8 @@ export async function getOAuthRedirectUrlDesktop( if (!port) { throw new OneKeyLocalError('OAuth local server returned invalid port.'); } - return `http://127.0.0.1:${port}/callback`; + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `http://127.0.0.1:${port}${OAUTH_CALLBACK_DESKTOP_PATH}`; } // Both WEBVIEW and DEEP_LINK methods use the same deep link URL diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx index 660542fb9f3e..3147d02a64c2 100644 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx +++ b/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx @@ -13,9 +13,12 @@ import { OAUTH_TOKEN_KEY_ACCESS_TOKEN, OAUTH_TOKEN_KEY_ID_TOKEN, OAUTH_TOKEN_KEY_REFRESH_TOKEN, + ONEKEY_OAUTH_STATE_KEY, } from '@onekeyhq/shared/src/consts/authConsts'; import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; +import { ensureOneKeyOAuthState } from './oauthUtils'; + import type { IExtensionOAuthConfig, IHandleOAuthSessionPersistenceParams, @@ -114,6 +117,7 @@ export async function openOAuthPopupExtIdentity(options: { type IExtensionOAuthFlowSignInParams = { rawNonce?: string; expectedState?: string; + expectedOneKeyState?: string; }; type IExtensionOAuthFlowGetAuthUrlResult = { authUrl: string; @@ -154,11 +158,14 @@ export async function openOAuthPopupExtIdentity(options: { const buildPkceFlowParams = ({ redirectUrl }: IExtensionOAuthFlowParams) => ({ getAuthUrl: async () => { + // Ensure redirectUrl contains ONEKEY_OAUTH_STATE_KEY parameter + const redirectUrlWithState = ensureOneKeyOAuthState(redirectUrl); + const oauthUrlResult = await client.auth.signInWithOAuth({ provider: 'google', options: { skipBrowserRedirect: true, - redirectTo: redirectUrl, + redirectTo: redirectUrlWithState, queryParams: { prompt: 'select_account', }, @@ -173,14 +180,30 @@ export async function openOAuthPopupExtIdentity(options: { throw new OneKeyLocalError('Failed to create Supabase OAuth URL.'); } let expectedState: string | undefined; + let expectedOneKeyState: string | undefined; try { - expectedState = new URL(authUrl).searchParams.get('state') ?? undefined; + const authUrlObj = new URL(authUrl); + expectedState = authUrlObj.searchParams.get('state') ?? undefined; + + // Parse our own state from the embedded redirect_to URL (defense-in-depth) + const redirectTo = authUrlObj.searchParams.get('redirect_to'); + if (redirectTo) { + try { + const redirectToUrl = new URL(redirectTo); + expectedOneKeyState = + redirectToUrl.searchParams.get(ONEKEY_OAUTH_STATE_KEY) ?? + undefined; + } catch { + expectedOneKeyState = undefined; + } + } } catch { expectedState = undefined; + expectedOneKeyState = undefined; } return { authUrl, - signInParams: { expectedState }, + signInParams: { expectedState, expectedOneKeyState }, }; }, signIn: async ({ @@ -201,6 +224,27 @@ export async function openOAuthPopupExtIdentity(options: { const code = url.searchParams.get('code'); const state = url.searchParams.get('state'); + const oneKeyState = url.searchParams.get(ONEKEY_OAUTH_STATE_KEY); + + if (!code) { + throw new OneKeyLocalError('Authorization code is missing'); + } + + // Validate OneKey state (defense-in-depth). + if (!oneKeyState) { + throw new OneKeyLocalError('OAuth state is missing'); + } + + if (!signInParams.expectedOneKeyState) { + throw new OneKeyLocalError('Expected OneKey OAuth state is missing'); + } + + if (oneKeyState !== signInParams.expectedOneKeyState) { + throw new OneKeyLocalError('OAuth state mismatch'); + } + + // Validate state (anti-CSRF / anti-injection). Supabase OAuth URLs should include `state=...` + // and the redirect callback should echo it back. if (signInParams.expectedState) { if (!state) { throw new OneKeyLocalError('OAuth state is missing'); @@ -209,9 +253,6 @@ export async function openOAuthPopupExtIdentity(options: { throw new OneKeyLocalError('OAuth state mismatch'); } } - if (!code) { - throw new OneKeyLocalError('Authorization code is missing'); - } const { data, error: exchangeError } = await client.auth.exchangeCodeForSession(code); diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx index fad64ade69d5..e44945b7d134 100644 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx +++ b/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx @@ -1,4 +1,5 @@ import { + OAUTH_CALLBACK_WEB_PATH, OAUTH_FLOW_TIMEOUT_MS, OAUTH_POLL_INTERVAL_MS, OAUTH_POPUP_HEIGHT, @@ -17,13 +18,14 @@ const ONEKEY_OAUTH_STATE_KEY = 'onekey_oauth_state'; /** * Get OAuth redirect URL for web platform * - * Uses the current origin with /auth/callback path - * Example: https://app.onekey.so/auth/callback + * Uses the current origin with /oauth_callback_web path + * Example: https://app.onekey.so/oauth_callback_web * * @returns The redirect URL for web OAuth */ export function getOAuthRedirectUrlWeb(): string { - return `${globalThis.location?.origin || ''}/auth/callback`; + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `${globalThis.location?.origin || ''}${OAUTH_CALLBACK_WEB_PATH}`; } // Focus the popup window to bring it to front, with error handling diff --git a/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx b/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx index e9d174cd5ba3..ed5c5012e1ef 100644 --- a/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx +++ b/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx @@ -10,12 +10,12 @@ import { EDesktopOAuthMethod, EExtensionOAuthMethod, GOOGLE_CHROME_EXTENSION_CLIENT_ID, - ONEKEY_OAUTH_STATE_KEY, } from '@onekeyhq/shared/src/consts/authConsts'; import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; import { ETranslations } from '@onekeyhq/shared/src/locale'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; +import { ensureOneKeyOAuthState } from '../oauthUtils'; import { getOAuthRedirectUrlDesktop, openOAuthPopupDesktopDeepLink, @@ -160,26 +160,7 @@ export function useSupabaseAuth() { DEFAULT_DESKTOP_OAUTH_METHOD === EDesktopOAuthMethod.LOCALHOST_SERVER)) ) { - try { - const redirectUrl = new URL(redirectTo); - if (!redirectUrl.searchParams.has(ONEKEY_OAUTH_STATE_KEY)) { - // Prefer crypto-grade random on web; if unavailable, skip rather than generating weak state. - const bytes = new Uint8Array(16); - const cryptoObj = globalThis.crypto as - | undefined - | { getRandomValues: (arr: Uint8Array) => Uint8Array }; - if (cryptoObj?.getRandomValues) { - cryptoObj.getRandomValues(bytes); - const state = Array.from(bytes) - .map((b) => b.toString(16).padStart(2, '0')) - .join(''); - redirectUrl.searchParams.set(ONEKEY_OAUTH_STATE_KEY, state); - redirectTo = redirectUrl.toString(); - } - } - } catch { - // ignore - } + redirectTo = ensureOneKeyOAuthState(redirectTo); } const oauthUrlResult = await clientTemp.auth.signInWithOAuth({ diff --git a/packages/shared/src/consts/authConsts.ts b/packages/shared/src/consts/authConsts.ts index e5ab264f10ee..5df4e9336fdd 100644 --- a/packages/shared/src/consts/authConsts.ts +++ b/packages/shared/src/consts/authConsts.ts @@ -8,6 +8,16 @@ const IS_DEV = process.env.NODE_ENV !== 'production'; export const OAUTH_CALLBACK_DESKTOP_CHANNEL = 'oauth:desktop_localhost_server:callback'; +/** + * OAuth callback path for desktop localhost server + */ +export const OAUTH_CALLBACK_DESKTOP_PATH = '/oauth_callback_desktop'; + +/** + * OAuth callback path for web platform + */ +export const OAUTH_CALLBACK_WEB_PATH = '/oauth_callback_web/'; + // ============================================================================ // OAuth shared constants (OneKeyAuth) // ============================================================================ From 2bbf4b5da8ca3c7b57c98a6f89b9da95bfa2d358 Mon Sep 17 00:00:00 2001 From: morizon Date: Sat, 20 Dec 2025 20:37:39 +0800 Subject: [PATCH 07/66] refactor: update OAuth server to use system-assigned ports and improve callback handling - Removed fixed port range for OAuth callbacks, allowing the server to listen on a dynamically assigned port. - Enhanced the OAuth callback handling logic to improve error responses and streamline the process of receiving authorization codes. - Updated documentation to reflect changes in the OAuth flow and the new method of handling redirect URLs. - Improved error messages for better user feedback during OAuth server startup. --- .../oauthLocalServer/oauthLocalServer.ts | 176 +++++++----------- .../OneKeyAuth/openOAuthPopupDesktop.tsx | 8 +- .../Components/stories/OneKeyIDGallery.tsx | 21 +++ 3 files changed, 95 insertions(+), 110 deletions(-) diff --git a/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts b/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts index 28b3a5614e90..30081f6ae750 100644 --- a/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts +++ b/apps/desktop/app/service/oauthLocalServer/oauthLocalServer.ts @@ -160,24 +160,6 @@ function tryOpenChromeAppWindow(url: string): boolean { return false; } -// Fixed port range for OAuth callback -// Web Application type requires explicit port configuration in Google Cloud Console -// These ports must be added to Authorized redirect URIs: -// http://localhost:19185/oauth_callback_desktop -// http://localhost:19285/oauth_callback_desktop -// http://localhost:19385/oauth_callback_desktop -// http://localhost:19485/oauth_callback_desktop -// http://localhost:19585/oauth_callback_desktop -// http://127.0.0.1:19185/oauth_callback_desktop -// http://127.0.0.1:19285/oauth_callback_desktop -// http://127.0.0.1:19385/oauth_callback_desktop -// http://127.0.0.1:19485/oauth_callback_desktop -// http://127.0.0.1:19585/oauth_callback_desktop -const OAUTH_PORTS = [ - 19_185, 19_285, 19_385, 19_485, 19_585, - // -]; - // Export functions for DesktopApiOAuth to use export async function startOAuthServer(): Promise<{ port: number }> { return new Promise((resolve, reject) => { @@ -187,103 +169,85 @@ export async function startOAuthServer(): Promise<{ port: number }> { oauthServer = null; } - // Try each port in sequence until one is available - let portIndex = 0; - const startIndex = Math.floor(Math.random() * OAUTH_PORTS.length); - - const tryStartServer = (): void => { - if (portIndex >= OAUTH_PORTS.length) { - reject(new Error('All OAuth ports are occupied')); - return; - } - - const port = OAUTH_PORTS[(startIndex + portIndex) % OAUTH_PORTS.length]; - oauthServer = createServer((req, res) => { - const url = new URL(req.url || '/', 'http://localhost'); - - // Handle callback from OAuth (Supabase redirects back to localhost with authorization code in URL query) - if (url.pathname === OAUTH_CALLBACK_DESKTOP_PATH) { - // PKCE flow: authorization code is in URL query string (not hash) - const error = url.searchParams.get('error'); + oauthServer = createServer((req, res) => { + const url = new URL(req.url || '/', 'http://localhost'); - if (error) { - // OAuth error occurred - res.writeHead(200, { - 'Content-Type': 'text/html; charset=utf-8', - }); - res.end(OAUTH_CALLBACK_ERROR_HTML(error)); - return; - } + // Handle callback from OAuth (Supabase redirects back to localhost with authorization code in URL query) + if (url.pathname === OAUTH_CALLBACK_DESKTOP_PATH) { + // PKCE flow: authorization code is in URL query string (not hash) + const error = url.searchParams.get('error'); - // Return HTML page that extracts code from URL query and sends to server + if (error) { + // OAuth error occurred res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', }); - res.end(OAUTH_CALLBACK_SUCCESS_HTML); - } else if (url.pathname === '/complete' && req.method === 'POST') { - // Receive authorization code from browser JS - let body = ''; - req.on('data', (chunk) => { - body += (chunk as Buffer).toString(); - }); - req.on('end', () => { - try { - const { code, state, oneKeyState } = JSON.parse(body) as { - code: string; - state?: string; - oneKeyState?: string; - }; - - if (code && mainWindow && !mainWindow.isDestroyed()) { - // Send authorization code to renderer process - mainWindow.webContents.send(OAUTH_CALLBACK_DESKTOP_CHANNEL, { - code, - state, - oneKeyState, - }); - } - - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('OK'); - - // Close server after receiving callback - setTimeout(() => { - oauthServer?.close(); - oauthServer = null; - }, 1000); - } catch (error) { - res.writeHead(400, { 'Content-Type': 'text/plain' }); - res.end('Invalid request'); - } - }); - } else { - // 404 for other paths - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('Not Found'); + res.end(OAUTH_CALLBACK_ERROR_HTML(error)); + return; } - }); - // Try to listen on the current port - oauthServer.listen(port, '127.0.0.1', () => { - resolve({ port }); - }); + // Return HTML page that extracts code from URL query and sends to server + res.writeHead(200, { + 'Content-Type': 'text/html; charset=utf-8', + }); + res.end(OAUTH_CALLBACK_SUCCESS_HTML); + } else if (url.pathname === '/complete' && req.method === 'POST') { + // Receive authorization code from browser JS + let body = ''; + req.on('data', (chunk) => { + body += (chunk as Buffer).toString(); + }); + req.on('end', () => { + try { + const { code, state, oneKeyState } = JSON.parse(body) as { + code: string; + state?: string; + oneKeyState?: string; + }; + + if (code && mainWindow && !mainWindow.isDestroyed()) { + // Send authorization code to renderer process + mainWindow.webContents.send(OAUTH_CALLBACK_DESKTOP_CHANNEL, { + code, + state, + oneKeyState, + }); + } - // eslint-disable-next-line spellcheck/spell-checker - oauthServer.on('error', (error: NodeJS.ErrnoException) => { - // If port is in use, try next port - if (error.code === 'EADDRINUSE') { - oauthServer?.close(); - oauthServer = null; - portIndex += 1; - tryStartServer(); - } else { - reject(error); - } - }); - }; + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('OK'); + + // Close server after receiving callback + setTimeout(() => { + oauthServer?.close(); + oauthServer = null; + }, 1000); + } catch (error) { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('Invalid request'); + } + }); + } else { + // 404 for other paths + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + } + }); + + // Use listen(0) to let the system automatically assign an available port + oauthServer.listen(0, '127.0.0.1', () => { + const address = oauthServer?.address(); + if (address && typeof address === 'object' && address.port) { + resolve({ port: address.port }); + } else { + reject(new Error('Failed to get assigned port from server')); + } + }); - // Start trying ports - tryStartServer(); + // eslint-disable-next-line spellcheck/spell-checker + oauthServer.on('error', (error: NodeJS.ErrnoException) => { + reject(error); + }); // Auto-close server after 5 minutes timeout setTimeout(() => { diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx index 758102632884..dacf40f4ab0b 100644 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx +++ b/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx @@ -29,13 +29,13 @@ import type { SupabaseClient } from '@supabase/supabase-js'; /** * OAuth helper for Desktop (Electron) platform using localhost HTTP server - * with Supabase OAuth redirecting back to localhost (fixed port range). + * with Supabase OAuth redirecting back to localhost (system-assigned port). * * This method uses Supabase as the OAuth intermediary with PKCE flow: * Google -> Supabase -> localhost callback (authorization code in URL query) * * How it works: - * 1. Main process starts a localhost HTTP server on a fixed port range + * 1. Main process starts a localhost HTTP server on a system-assigned port (listen(0)) * 2. Renderer opens Supabase OAuth URL in system browser (skipBrowserRedirect=true) * 3. Supabase handles Google OAuth and redirects back with authorization code * 4. Main process extracts code from URL query and sends it to renderer via IPC @@ -43,7 +43,7 @@ import type { SupabaseClient } from '@supabase/supabase-js'; * 6. Renderer persists session via handleSessionPersistence * * Supabase Configuration: - * - Add Redirect URLs (fixed port range) + * - Redirect URL is dynamically constructed with the assigned port * * @param options - Configuration options * @param options.authUrl - Supabase OAuth URL (skipBrowserRedirect=true) @@ -343,7 +343,7 @@ export async function getOAuthRedirectUrlDesktop( port = serverResult.port; } catch (e) { throw new OneKeyLocalError( - 'OAuth local ports are occupied. Please close conflicting apps and try again.', + 'Failed to start OAuth local server. Please try again.', ); } if (!port) { diff --git a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx index e8d71731f5f6..2d71f5d72683 100644 --- a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx +++ b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx @@ -18,6 +18,7 @@ import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/background import { useSupabaseAuth } from '@onekeyhq/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth'; import { useOneKeyAuth } from '@onekeyhq/kit/src/components/OneKeyAuth/useOneKeyAuth'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; +import { formatDate } from '@onekeyhq/shared/src/utils/dateUtils'; import stringUtils from '@onekeyhq/shared/src/utils/stringUtils'; import { Layout } from './utils/Layout'; @@ -289,6 +290,26 @@ function OneKeyIDApiTests() { {decodedToken !== null ? ( Decoded Access Token + {decodedToken.iat && typeof decodedToken.iat === 'number' ? ( + + + Issued at: + + + {formatDate(new Date(decodedToken.iat * 1000))} + + + ) : null} + {decodedToken.exp && typeof decodedToken.exp === 'number' ? ( + + + Expires at: + + + {formatDate(new Date(decodedToken.exp * 1000))} + + + ) : null} Date: Sat, 20 Dec 2025 23:27:09 +0800 Subject: [PATCH 08/66] refactor: streamline OAuth flow in Chrome extension - Introduced a new `getRedirectUrl` function to standardize the retrieval of the OAuth redirect URL, ensuring it matches Google Cloud Console configuration. - Updated `openOAuthPopupExtIdentity` to utilize the new redirect URL function and accept an optional `authUrl` parameter for improved flexibility. - Refactored the OAuth flow processing to simplify the handling of authentication URLs and session management. - Enhanced `useSupabaseAuth` to support the new `authUrl` parameter, aligning the extension's OAuth handling with web standards. --- .../OneKeyAuth/openOAuthPopupExt.tsx | 105 +++++++----------- .../OneKeyAuth/supabase/useSupabaseAuth.tsx | 36 +++--- 2 files changed, 57 insertions(+), 84 deletions(-) diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx index 3147d02a64c2..e9d87e5b607a 100644 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx +++ b/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx @@ -29,7 +29,15 @@ import type { SupabaseClient } from '@supabase/supabase-js'; // ============================================================================ // Extension OAuth Methods // ============================================================================ - +const getRedirectUrl = (): string => { + // https://.chromiumapp.org/ + let redirectUrl = chrome.identity.getRedirectURL(); + // Remove trailing slash to match Google Cloud Console configuration (and existing behavior). + if (redirectUrl.endsWith('/')) { + redirectUrl = redirectUrl.slice(0, -1); + } + return redirectUrl; +}; /** * Get OAuth redirect URL for Chrome Extension * @@ -48,8 +56,7 @@ export function getOAuthRedirectUrlExt( method === EExtensionOAuthMethod.CHROME_IDENTITY_API || method === EExtensionOAuthMethod.CHROME_GET_AUTH_TOKEN ) { - // These methods handle redirect URL internally, not needed externally - return undefined; + return getRedirectUrl(); } // Use direct chrome-extension:// scheme // Format: chrome-extension:///ui-oauth-callback.html @@ -90,8 +97,15 @@ export async function openOAuthPopupExtIdentity(options: { params: IHandleOAuthSessionPersistenceParams, ) => Promise; persistSession?: boolean; + authUrl?: string; }): Promise { - const { client, config, handleSessionPersistence, persistSession } = options; + const { + client, + config, + handleSessionPersistence, + persistSession, + authUrl: externalAuthUrl, + } = options; const { googleClientId, scopes = GOOGLE_OAUTH_DEFAULT_SCOPES } = config; if (!chrome.identity) { @@ -103,16 +117,6 @@ export async function openOAuthPopupExtIdentity(options: { ); } - const getRedirectUrl = (): string => { - // https://.chromiumapp.org/ - let redirectUrl = chrome.identity.getRedirectURL(); - // Remove trailing slash to match Google Cloud Console configuration (and existing behavior). - if (redirectUrl.endsWith('/')) { - redirectUrl = redirectUrl.slice(0, -1); - } - return redirectUrl; - }; - type IExtensionOAuthFlowParams = { redirectUrl: string }; type IExtensionOAuthFlowSignInParams = { rawNonce?: string; @@ -158,24 +162,7 @@ export async function openOAuthPopupExtIdentity(options: { const buildPkceFlowParams = ({ redirectUrl }: IExtensionOAuthFlowParams) => ({ getAuthUrl: async () => { - // Ensure redirectUrl contains ONEKEY_OAUTH_STATE_KEY parameter - const redirectUrlWithState = ensureOneKeyOAuthState(redirectUrl); - - const oauthUrlResult = await client.auth.signInWithOAuth({ - provider: 'google', - options: { - skipBrowserRedirect: true, - redirectTo: redirectUrlWithState, - queryParams: { - prompt: 'select_account', - }, - }, - }); - - if (oauthUrlResult.error) { - throw new OneKeyLocalError(oauthUrlResult.error.message); - } - const authUrl = oauthUrlResult.data?.url; + const authUrl = externalAuthUrl; if (!authUrl) { throw new OneKeyLocalError('Failed to create Supabase OAuth URL.'); } @@ -285,25 +272,6 @@ export async function openOAuthPopupExtIdentity(options: { }, }); - const processFlow = async ({ - redirectUrl, - buildFlowParams, - }: IExtensionOAuthFlowParams & { - buildFlowParams: IExtensionOAuthFlowBuilder; - }) => { - const { getAuthUrl, signIn } = buildFlowParams({ redirectUrl }); - const { authUrl, signInParams } = await getAuthUrl(); - const callbackUrl = await launchWebAuthFlowWithTimeout(authUrl); - - if (!callbackUrl) { - throw new OneKeyLocalError( - 'OAuth authentication failed: callback URL is missing', - ); - } - - return signIn({ callbackUrl, signInParams }); - }; - const buildOidcFlowParams = ({ redirectUrl }: IExtensionOAuthFlowParams) => ({ getAuthUrl: async () => { // Build Google OAuth URL manually with response_type=id_token @@ -392,6 +360,28 @@ export async function openOAuthPopupExtIdentity(options: { }, }); + const processFlow = async () => { + const redirectUrl = getRedirectUrl(); + + const { getAuthUrl, signIn } = EXTENSION_OAUTH_USE_PKCE_FLOW + ? buildPkceFlowParams({ redirectUrl }) + : buildOidcFlowParams({ redirectUrl }); + + const result = await getAuthUrl(); + const authUrl = result.authUrl; + const signInParams = result.signInParams; + + const callbackUrl = await launchWebAuthFlowWithTimeout(authUrl); + + if (!callbackUrl) { + throw new OneKeyLocalError( + 'OAuth authentication failed: callback URL is missing', + ); + } + + return signIn({ callbackUrl, signInParams }); + }; + // Launch the OAuth flow // Note: chrome.identity.launchWebAuthFlow doesn't support window size/position options // Chrome controls the OAuth window and may not allow modifications @@ -455,8 +445,6 @@ export async function openOAuthPopupExtIdentity(options: { } }; - const redirectUrl = getRedirectUrl(); - try { // -------------------------------------------------------------------------- // PKCE mode (Supabase OAuth URL + exchangeCodeForSession) @@ -468,16 +456,7 @@ export async function openOAuthPopupExtIdentity(options: { // When true, use Supabase OAuth + PKCE code flow and exchange the returned code for a session. // When false (default), use Google OIDC id_token + nonce and exchange via signInWithIdToken(). // -------------------------------------------------------------------------- - if (EXTENSION_OAUTH_USE_PKCE_FLOW) { - return await processFlow({ - redirectUrl, - buildFlowParams: buildPkceFlowParams, - }); - } - return await processFlow({ - redirectUrl, - buildFlowParams: buildOidcFlowParams, - }); + return await processFlow(); } catch (error) { // User closed the popup or other error if ( diff --git a/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx b/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx index ed5c5012e1ef..0c22b0366c40 100644 --- a/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx +++ b/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx @@ -109,19 +109,6 @@ export function useSupabaseAuth() { // we don't need Supabase OAuth URL - these methods build their own Google OAuth URL // and use signInWithIdToken instead if (platformEnv.isExtension) { - if ( - DEFAULT_EXTENSION_OAUTH_METHOD === - EExtensionOAuthMethod.CHROME_IDENTITY_API - ) { - // Use launchWebAuthFlow + signInWithIdToken (Supabase recommended) - // This method builds its own Google OAuth URL with response_type=id_token - return openOAuthPopupExtIdentity({ - client: clientTemp, - config: { googleClientId: GOOGLE_CHROME_EXTENSION_CLIENT_ID }, - handleSessionPersistence: handleOAuthSessionPersistence, - persistSession, - }); - } if ( DEFAULT_EXTENSION_OAUTH_METHOD === EExtensionOAuthMethod.CHROME_GET_AUTH_TOKEN @@ -152,14 +139,7 @@ export function useSupabaseAuth() { // Defense-in-depth: Supabase PKCE URL may not include `state`. We embed our own // nonce into redirectTo so the callback must carry it back to us. - if ( - redirectTo && - (platformEnv.isWeb || - // Desktop localhost server method - (platformEnv.isDesktop && - DEFAULT_DESKTOP_OAUTH_METHOD === - EDesktopOAuthMethod.LOCALHOST_SERVER)) - ) { + if (redirectTo) { redirectTo = ensureOneKeyOAuthState(redirectTo); } @@ -225,6 +205,20 @@ export function useSupabaseAuth() { // For extension with DIRECT_EXTENSION_SCHEME (does not work, kept for reference) if (platformEnv.isExtension) { + if ( + DEFAULT_EXTENSION_OAUTH_METHOD === + EExtensionOAuthMethod.CHROME_IDENTITY_API + ) { + // Use launchWebAuthFlow + signInWithIdToken (Supabase recommended) + // Pass authUrl from external creation (similar to web version) + return openOAuthPopupExtIdentity({ + client: clientTemp, + config: { googleClientId: GOOGLE_CHROME_EXTENSION_CLIENT_ID }, + handleSessionPersistence: handleOAuthSessionPersistence, + persistSession, + authUrl, + }); + } return openOAuthPopupExtWindow({ authUrl, handleSessionPersistence: handleOAuthSessionPersistence, From 39bece3ba2f571df6178f88c11d4c74bbf371fc0 Mon Sep 17 00:00:00 2001 From: morizon Date: Mon, 22 Dec 2025 16:51:49 +0800 Subject: [PATCH 09/66] refactor: consolidate OAuth handling across platforms - Removed OAuth2 configuration from Chrome manifest files, streamlining the setup process. - Simplified the `IHandleOAuthSessionPersistenceParams` type by removing unnecessary properties. - Introduced a unified `OAuthPopup` class structure for handling OAuth across web, desktop, extension, and native platforms. - Enhanced `useSupabaseAuth` to leverage the new `OAuthPopup` implementation, improving session management and code clarity. - Updated various components to ensure compatibility with the new OAuth structure and improve overall maintainability. --- apps/ext/src/manifest/chrome.js | 13 +- apps/ext/src/manifest/chrome_v3.js | 13 - .../OAuthPopup/OAuthPopup.desktop.tsx | 264 ++++++++++++ .../OneKeyAuth/OAuthPopup/OAuthPopup.ext.tsx | 397 ++++++++++++++++++ .../OAuthPopup/OAuthPopup.native.tsx | 316 ++++++++++++++ .../OneKeyAuth/OAuthPopup/OAuthPopup.tsx | 264 ++++++++++++ .../OneKeyAuth/OAuthPopup/OAuthPopupBase.ts | 201 +++++++++ .../components/OneKeyAuth/OAuthPopup/index.ts | 21 + .../components/OneKeyAuth/OAuthPopup/types.ts | 67 +++ .../OneKeyAuth/openOAuthPopupTypes.tsx | 3 - .../OneKeyAuth/openOAuthPopupWeb.tsx | 4 - .../OneKeyAuth/supabase/useSupabaseAuth.tsx | 179 ++------ packages/shared/src/consts/authConsts.ts | 26 +- 13 files changed, 1585 insertions(+), 183 deletions(-) create mode 100644 packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.desktop.tsx create mode 100644 packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.ext.tsx create mode 100644 packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx create mode 100644 packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.tsx create mode 100644 packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopupBase.ts create mode 100644 packages/kit/src/components/OneKeyAuth/OAuthPopup/index.ts create mode 100644 packages/kit/src/components/OneKeyAuth/OAuthPopup/types.ts diff --git a/apps/ext/src/manifest/chrome.js b/apps/ext/src/manifest/chrome.js index 5dd39082555a..295df825a553 100644 --- a/apps/ext/src/manifest/chrome.js +++ b/apps/ext/src/manifest/chrome.js @@ -74,18 +74,7 @@ module.exports = { // 'webRequest', 'idle', ], - // OAuth2 configuration for chrome.identity.getAuthToken - // Required for CHROME_GET_AUTH_TOKEN method - 'oauth2': { - 'client_id': - process.env.GOOGLE_CHROME_EXTENSION_CLIENT_ID || - '244450898872-foi2b6mtfqus1ed46hu5j03abne6b04s.apps.googleusercontent.com', - 'scopes': [ - 'openid', - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile', - ], - }, + }; /* action:{ diff --git a/apps/ext/src/manifest/chrome_v3.js b/apps/ext/src/manifest/chrome_v3.js index 0a89ea1c96f6..bd994c20642e 100644 --- a/apps/ext/src/manifest/chrome_v3.js +++ b/apps/ext/src/manifest/chrome_v3.js @@ -124,17 +124,4 @@ module.exports = { 'sidePanel', 'contextMenus', ], - // OAuth2 configuration for chrome.identity.getAuthToken - // Required for CHROME_GET_AUTH_TOKEN method - // The client_id should be a Chrome Extension type OAuth client from Google Cloud Console - 'oauth2': { - 'client_id': - process.env.GOOGLE_CHROME_EXTENSION_CLIENT_ID || - '244450898872-foi2b6mtfqus1ed46hu5j03abne6b04s.apps.googleusercontent.com', - 'scopes': [ - 'openid', - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile', - ], - }, }; diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.desktop.tsx b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.desktop.tsx new file mode 100644 index 000000000000..0f3241c3f836 --- /dev/null +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.desktop.tsx @@ -0,0 +1,264 @@ +import { Dialog } from '@onekeyhq/components'; +import type { IDialogInstance } from '@onekeyhq/components'; +import { + OAUTH_CALLBACK_DESKTOP_CHANNEL, + OAUTH_CALLBACK_DESKTOP_PATH, + OAUTH_FLOW_TIMEOUT_MS, +} from '@onekeyhq/shared/src/consts/authConsts'; +import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; +import platformEnv from '@onekeyhq/shared/src/platformEnv'; + +import { OAuthPopupBase } from './OAuthPopupBase'; + +import type { IOAuthPopupOptions, IOAuthPopupResult } from './types'; + +// ============================================================================ +// Desktop OAuth Popup Implementation +// ============================================================================ + +/** + * OAuth popup implementation for Desktop (Electron) platform. + * + * Uses localhost HTTP server for OAuth callback (primary method). + * Opens OAuth URL in system browser and listens for callback via IPC. + * + * Flow: + * 1. Start localhost HTTP server on system-assigned port + * 2. Open Supabase OAuth URL in system browser + * 3. User completes OAuth in browser + * 4. Browser redirects to localhost callback with authorization code + * 5. Exchange code for session using Supabase PKCE + */ +export class OAuthPopup extends OAuthPopupBase { + // ============ Public API ============ + + /** + * Get OAuth redirect URL for Desktop platform. + * + * Starts localhost OAuth server and returns callback URL. + * Returns: http://127.0.0.1:{port}/oauth/callback + */ + static override async getRedirectUrl(): Promise { + if ( + !platformEnv.isDesktop || + !globalThis.desktopApiProxy?.oauthLocalServer + ) { + throw new OneKeyLocalError( + 'Desktop OAuth Local Server API is not available', + ); + } + + let port = 0; + try { + const serverResult = + await globalThis.desktopApiProxy.oauthLocalServer.startServer(); + port = serverResult.port; + } catch { + throw new OneKeyLocalError( + 'Failed to start OAuth local server. Please try again.', + ); + } + + if (!port) { + throw new OneKeyLocalError('OAuth local server returned invalid port.'); + } + + return `http://127.0.0.1:${port}${OAUTH_CALLBACK_DESKTOP_PATH}`; + } + + /** + * Open OAuth using localhost HTTP server. + * + * Opens OAuth URL in system browser and listens for callback via IPC. + */ + static override async open( + options: IOAuthPopupOptions, + ): Promise { + const { authUrl, client, handleSessionPersistence } = options; + + if (!authUrl) { + throw new OneKeyLocalError('OAuth URL is required'); + } + + if (!client) { + throw new OneKeyLocalError('Supabase client is required'); + } + + if ( + !platformEnv.isDesktop || + !globalThis.desktopApiProxy?.oauthLocalServer + ) { + throw new OneKeyLocalError( + 'Desktop OAuth Local Server API is not available', + ); + } + + // Parse expected states for validation + const { expectedState, expectedOneKeyState } = + OAuthPopup.parseExpectedStates(authUrl); + + return new Promise((resolve, reject) => { + void (async () => { + let settled = false; + let timeoutId: ReturnType | null = null; + let dialogClosed = false; + let waitingDialog: IDialogInstance | null = null; + + // Define cleanup first to avoid "used before defined" error + const cleanupFn = { + cleanup: async () => {}, + }; + + // IPC callback handler + const handleCallback = async ( + _event: Electron.IpcRendererEvent, + data: { + code?: string; + state?: string; + oneKeyState?: string; + }, + ) => { + if (settled) { + return; + } + settled = true; + + // Remove listener + if (globalThis.desktopApi) { + globalThis.desktopApi.removeIpcEventListener( + OAUTH_CALLBACK_DESKTOP_CHANNEL, + handleCallback, + ); + } + + try { + dialogClosed = true; + await Promise.resolve(waitingDialog?.close()); + + const { code, state, oneKeyState } = data; + + if (!code) { + await cleanupFn.cleanup(); + reject(new OneKeyLocalError('Authorization code is missing')); + return; + } + + // Validate states + OAuthPopup.validateOneKeyState( + expectedOneKeyState, + oneKeyState ?? null, + ); + OAuthPopup.validateSupabaseState(expectedState, state ?? null); + + // Exchange code for session + const { accessToken, refreshToken } = + await OAuthPopup.exchangeCodeForSession(client, code); + + // Handle session persistence + await handleSessionPersistence({ + accessToken, + refreshToken, + }); + + await cleanupFn.cleanup(); + resolve({ + success: true, + session: { accessToken, refreshToken }, + }); + } catch (error) { + await cleanupFn.cleanup(); + reject(OAuthPopup.wrapError(error, 'OAuth failed')); + } + }; + + // Assign cleanup implementation + cleanupFn.cleanup = async () => { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + if (globalThis.desktopApi) { + globalThis.desktopApi.removeIpcEventListener( + OAUTH_CALLBACK_DESKTOP_CHANNEL, + handleCallback, + ); + } + try { + await globalThis.desktopApiProxy.oauthLocalServer.stopServer(); + } catch { + // Ignore stop errors + } + try { + if (!dialogClosed) { + await Promise.resolve(waitingDialog?.close()); + } + } catch { + // Ignore close errors + } + }; + + try { + // Show waiting dialog + waitingDialog = Dialog.show({ + title: 'Sign in', + description: + 'Complete sign-in in your browser, then return to OneKey.', + showFooter: true, + showConfirmButton: false, + showCancelButton: true, + onCancel: async (close) => { + if (settled) { + await close(); + return; + } + settled = true; + dialogClosed = true; + await close(); + await cleanupFn.cleanup(); + reject(new OneKeyLocalError('OAuth sign-in was cancelled')); + }, + onClose: async (extra) => { + if (extra?.flag === 'cancel' && !settled) { + settled = true; + dialogClosed = true; + await cleanupFn.cleanup(); + reject(new OneKeyLocalError('OAuth sign-in was cancelled')); + } + }, + }); + + // Add IPC listener + if (globalThis.desktopApi) { + globalThis.desktopApi.addIpcEventListener( + OAUTH_CALLBACK_DESKTOP_CHANNEL, + handleCallback, + ); + } + + // Open OAuth URL in system browser + await globalThis.desktopApiProxy.oauthLocalServer.openBrowser( + authUrl, + ); + + // Setup timeout + timeoutId = setTimeout(() => { + if (settled) { + return; + } + settled = true; + void cleanupFn.cleanup().finally(() => { + reject(new OneKeyLocalError('OAuth sign-in timed out')); + }); + }, OAUTH_FLOW_TIMEOUT_MS); + } catch (error) { + Dialog.debugMessage({ + title: 'OAuth', + debugMessage: + error instanceof Error ? error.message : 'OAuth setup failed', + }); + reject(OAuthPopup.wrapError(error, 'OAuth setup failed')); + } + })(); + }); + } +} diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.ext.tsx b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.ext.tsx new file mode 100644 index 000000000000..7e8a4bdcda9a --- /dev/null +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.ext.tsx @@ -0,0 +1,397 @@ +/* eslint-disable spellcheck/spell-checker */ +import { + EXTENSION_OAUTH_USE_PKCE_FLOW, + GOOGLE_OAUTH_AUTHORIZE_URL, + GOOGLE_OAUTH_CLIENT_IDS, + GOOGLE_OAUTH_DEFAULT_SCOPES, + OAUTH_FLOW_TIMEOUT_MS, + OAUTH_POLL_INTERVAL_MS, + OAUTH_POPUP_HEIGHT, + OAUTH_POPUP_WIDTH, + OAUTH_TOKEN_KEY_ID_TOKEN, + ONEKEY_OAUTH_STATE_KEY, +} from '@onekeyhq/shared/src/consts/authConsts'; +import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; + +import { OAuthPopupBase } from './OAuthPopupBase'; + +import type { IOAuthPopupOptions, IOAuthPopupResult } from './types'; +import type { SupabaseClient } from '@supabase/supabase-js'; + +// ============================================================================ +// Internal Types +// ============================================================================ + +interface IExtensionOAuthFlowSignInParams { + rawNonce?: string; + expectedState?: string; + expectedOneKeyState?: string; +} + +interface IExtensionOAuthFlowGetAuthUrlResult { + authUrl: string; + signInParams: IExtensionOAuthFlowSignInParams; +} + +interface IExtensionOAuthFlowSignInInput { + callbackUrl: string; + signInParams: IExtensionOAuthFlowSignInParams; +} + +interface IExtensionOAuthFlowBuilder { + getAuthUrl: () => Promise; + signIn: (input: IExtensionOAuthFlowSignInInput) => Promise; +} + +// ============================================================================ +// Extension OAuth Popup Implementation +// ============================================================================ + +/** + * OAuth popup implementation for Chrome Extension platform. + * + * Uses chrome.identity.launchWebAuthFlow for OAuth authentication. + * Supports two flows: + * - PKCE flow: Supabase OAuth URL + exchangeCodeForSession + * - OIDC flow: Google id_token + signInWithIdToken + */ +export class OAuthPopup extends OAuthPopupBase { + // ============ Public API ============ + + /** + * Get OAuth redirect URL for Chrome Extension. + * + * Returns: https://.chromiumapp.org + */ + static override getRedirectUrl(): Promise { + let redirectUrl = chrome.identity.getRedirectURL(); + // Remove trailing slash to match Google Cloud Console configuration + if (redirectUrl.endsWith('/')) { + redirectUrl = redirectUrl.slice(0, -1); + } + return Promise.resolve(redirectUrl); + } + + /** + * Open OAuth using chrome.identity.launchWebAuthFlow. + * + * Internally selects PKCE or OIDC flow based on EXTENSION_OAUTH_USE_PKCE_FLOW config. + */ + static override async open( + options: IOAuthPopupOptions, + ): Promise { + const { client, handleSessionPersistence, authUrl, redirectTo } = options; + + if (!chrome.identity) { + throw new OneKeyLocalError( + 'chrome.identity API is not available. ' + + 'Make sure you are running in a Chrome Extension context (not content script) ' + + 'and the "identity" permission is added to manifest.json.', + ); + } + + if (!redirectTo) { + throw new OneKeyLocalError( + 'redirectTo is required. Call OAuthPopup.getRedirectUrl() first.', + ); + } + + const redirectUrl = redirectTo; + + // Build flow params based on config + const flowBuilder = EXTENSION_OAUTH_USE_PKCE_FLOW + ? OAuthPopup.buildPkceFlowParams(redirectUrl, client, authUrl) + : OAuthPopup.buildOidcFlowParams(redirectUrl, client); + + // Setup window focus listener + const cleanupFocus = OAuthPopup.setupWindowFocusListener(); + + try { + const { authUrl: oauthUrl, signInParams } = + await flowBuilder.getAuthUrl(); + const callbackUrl = await OAuthPopup.launchWebAuthFlowWithTimeout( + oauthUrl, + ); + + if (!callbackUrl) { + throw new OneKeyLocalError( + 'OAuth authentication failed: callback URL is missing', + ); + } + + const result = await flowBuilder.signIn({ callbackUrl, signInParams }); + + // Handle session persistence + if (result.success && result.session) { + await handleSessionPersistence({ + accessToken: result.session.accessToken, + refreshToken: result.session.refreshToken, + }); + } + + return result; + } catch (error) { + if (OAuthPopup.isUserCancelledError(error)) { + throw new OneKeyLocalError('OAuth sign-in was cancelled'); + } + throw OAuthPopup.wrapError(error, 'Extension OAuth failed'); + } finally { + try { + cleanupFocus(); + } catch { + // Ignore cleanup errors + } + } + } + + // ============ Private Methods ============ + + /** + * Launch chrome.identity.launchWebAuthFlow with timeout. + */ + private static async launchWebAuthFlowWithTimeout( + url: string, + ): Promise { + let timeoutId: ReturnType | null = null; + + try { + return await Promise.race([ + chrome.identity.launchWebAuthFlow({ + url, + interactive: true, + }), + new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject(new OneKeyLocalError('OAuth sign-in timed out')); + }, OAUTH_FLOW_TIMEOUT_MS); + }), + ]); + } finally { + if (timeoutId) { + clearTimeout(timeoutId); + } + } + } + + /** + * Build PKCE flow params (Supabase OAuth + exchangeCodeForSession). + */ + private static buildPkceFlowParams( + redirectUrl: string, + client: SupabaseClient | undefined, + externalAuthUrl: string | undefined, + ): IExtensionOAuthFlowBuilder { + return { + getAuthUrl: async () => { + if (!externalAuthUrl) { + throw new OneKeyLocalError('Failed to create Supabase OAuth URL.'); + } + + const { expectedState, expectedOneKeyState } = + OAuthPopup.parseExpectedStates(externalAuthUrl); + + return { + authUrl: externalAuthUrl, + signInParams: { + expectedState: expectedState ?? undefined, + expectedOneKeyState: expectedOneKeyState ?? undefined, + }, + }; + }, + + signIn: async ({ callbackUrl, signInParams }) => { + if (!client) { + throw new OneKeyLocalError( + 'Supabase client is required for PKCE flow', + ); + } + + const url = new URL(callbackUrl); + + if (!callbackUrl.startsWith(redirectUrl)) { + throw new OneKeyLocalError('Invalid OAuth redirect URL'); + } + + const error = + url.searchParams.get('error') || + url.searchParams.get('error_description'); + if (error) { + throw new OneKeyLocalError(error); + } + + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + const oneKeyState = url.searchParams.get(ONEKEY_OAUTH_STATE_KEY); + + if (!code) { + throw new OneKeyLocalError('Authorization code is missing'); + } + + // Validate states + OAuthPopup.validateOneKeyState( + signInParams.expectedOneKeyState ?? null, + oneKeyState, + ); + OAuthPopup.validateSupabaseState( + signInParams.expectedState ?? null, + state, + ); + + // Exchange code for session + const { accessToken, refreshToken } = + await OAuthPopup.exchangeCodeForSession(client, code); + + return { + success: true, + session: { accessToken, refreshToken }, + }; + }, + }; + } + + /** + * Build OIDC flow params (Google id_token + signInWithIdToken). + */ + private static buildOidcFlowParams( + redirectUrl: string, + client: SupabaseClient | undefined, + ): IExtensionOAuthFlowBuilder { + const scopes = GOOGLE_OAUTH_DEFAULT_SCOPES; + + return { + getAuthUrl: async () => { + // Build Google OAuth URL manually with response_type=id_token + const authUrl = new URL(GOOGLE_OAUTH_AUTHORIZE_URL); + + authUrl.searchParams.set( + 'client_id', + GOOGLE_OAUTH_CLIENT_IDS.EXTENSION, + ); + authUrl.searchParams.set('response_type', 'id_token'); + authUrl.searchParams.set('access_type', 'offline'); + authUrl.searchParams.set('redirect_uri', redirectUrl); + authUrl.searchParams.set('scope', scopes.join(' ')); + + // Generate and hash nonce + const rawNonce = crypto.randomUUID(); + const encoder = new TextEncoder(); + const nonceData = encoder.encode(rawNonce); + const hashBuffer = await crypto.subtle.digest('SHA-256', nonceData); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashedNonce = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + + authUrl.searchParams.set('nonce', hashedNonce); + authUrl.searchParams.set('prompt', 'select_account'); + + return { + authUrl: authUrl.href, + signInParams: { rawNonce }, + }; + }, + + signIn: async ({ callbackUrl, signInParams }) => { + if (!client) { + throw new OneKeyLocalError( + 'Supabase client is required for OIDC flow', + ); + } + + const { rawNonce } = signInParams; + if (!rawNonce) { + throw new OneKeyLocalError('Missing nonce for Google OAuth sign-in.'); + } + + // Parse id_token from callback URL hash + const url = new URL(callbackUrl); + const hashParams = new URLSearchParams(url.hash.substring(1)); + const idToken = hashParams.get(OAUTH_TOKEN_KEY_ID_TOKEN); + + if (!idToken) { + throw new OneKeyLocalError('No ID token received from Google OAuth'); + } + + // Exchange ID token for Supabase session + const { data, error } = await client.auth.signInWithIdToken({ + provider: 'google', + token: idToken, + nonce: rawNonce, + }); + + if (error) { + throw new OneKeyLocalError(error.message); + } + + if (!data.session) { + throw new OneKeyLocalError('Failed to exchange ID token for session'); + } + + return { + success: true, + session: { + accessToken: data.session.access_token, + refreshToken: data.session.refresh_token, + }, + }; + }, + }; + } + + /** + * Setup window focus listener for OAuth popup. + * Returns cleanup function. + */ + private static setupWindowFocusListener(): () => void { + const width = OAUTH_POPUP_WIDTH; + const height = OAUTH_POPUP_HEIGHT; + const left = Math.round((globalThis.screen?.width || 1920) / 2 - width / 2); + const top = Math.round( + (globalThis.screen?.height || 1080) / 2 - height / 2, + ); + + let focusInterval: ReturnType | null = null; + let oauthWindowId: number | null = null; + + const windowUpdateListener = (window: chrome.windows.Window) => { + if (window.type === 'popup' && window.id) { + oauthWindowId = window.id; + + // Try to update window size and position + chrome.windows + .update(window.id, { + width, + height, + left, + top, + focused: true, + }) + .catch(() => { + // Chrome may not allow updating OAuth windows + }); + + // Set up focus polling + focusInterval = setInterval(() => { + if (oauthWindowId !== null) { + chrome.windows + .update(oauthWindowId, { focused: true }) + .catch(() => { + // Ignore focus errors + }); + } + }, OAUTH_POLL_INTERVAL_MS); + + chrome.windows.onCreated.removeListener(windowUpdateListener); + } + }; + + chrome.windows.onCreated.addListener(windowUpdateListener); + + return () => { + chrome.windows.onCreated.removeListener(windowUpdateListener); + if (focusInterval !== null) { + clearInterval(focusInterval); + } + }; + } +} diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx new file mode 100644 index 000000000000..64b7417833c3 --- /dev/null +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx @@ -0,0 +1,316 @@ +/* eslint-disable spellcheck/spell-checker */ +import { GoogleSignin } from '@react-native-google-signin/google-signin'; +import * as WebBrowser from 'expo-web-browser'; + +import { + DEFAULT_NATIVE_OAUTH_METHOD, + ENativeOAuthMethod, + GOOGLE_OAUTH_CLIENT_IDS, + OAUTH_TOKEN_KEY_ACCESS_TOKEN, + OAUTH_TOKEN_KEY_REFRESH_TOKEN, +} from '@onekeyhq/shared/src/consts/authConsts'; +import { ONEKEY_APP_DEEP_LINK } from '@onekeyhq/shared/src/consts/deeplinkConsts'; +import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; +import platformEnv from '@onekeyhq/shared/src/platformEnv'; + +import { OAuthPopupBase } from './OAuthPopupBase'; + +import type { + INativeOAuthConfig, + IOAuthPopupOptions, + IOAuthPopupResult, +} from './types'; + +// ============================================================================ +// Native OAuth Popup Implementation +// ============================================================================ + +/** + * OAuth popup implementation for native platforms (iOS/Android). + * + * Supports two methods: + * - GOOGLE_SIGNIN (default): Uses @react-native-google-signin for native Google Sign-In + * - WEB_BROWSER (fallback): Uses expo-web-browser for in-app browser OAuth + * + * The GOOGLE_SIGNIN method provides better UX with native UI and is recommended + * per Supabase documentation for React Native: + * https://supabase.com/docs/guides/auth/social-login/auth-google?platform=react-native + */ +export class OAuthPopup extends OAuthPopupBase { + // ============ Public API ============ + + /** + * Get OAuth redirect URL for native platforms. + * + * Uses the deep link scheme: onekey-wallet://auth/callback + * Note: This is only used for WEB_BROWSER method. + * GOOGLE_SIGNIN method doesn't need a redirect URL. + */ + static override getRedirectUrl(): Promise { + return Promise.resolve(`${ONEKEY_APP_DEEP_LINK}auth/callback`); + } + + /** + * Open OAuth and return result. + * + * Uses GOOGLE_SIGNIN method by default for native Google Sign-In experience. + * Falls back to WEB_BROWSER method if GoogleSignin is not available. + */ + static override async open( + options: IOAuthPopupOptions, + ): Promise { + const method = DEFAULT_NATIVE_OAUTH_METHOD; + + // Try GoogleSignin first (default) + if (method === ENativeOAuthMethod.GOOGLE_SIGNIN) { + try { + return await OAuthPopup.openWithGoogleSignin(options); + } catch (error) { + // If GoogleSignin fails due to setup issues, fall back to WebBrowser + if (OAuthPopup.shouldFallbackToWebBrowser(error)) { + console.warn( + 'GoogleSignin not available, falling back to WebBrowser:', + error instanceof Error ? error.message : error, + ); + return OAuthPopup.openWithWebBrowser(options); + } + throw error; + } + } + + // Use WebBrowser method + return OAuthPopup.openWithWebBrowser(options); + } + + // ============ Private Methods - GoogleSignin ============ + + /** + * Check if error indicates GoogleSignin is not properly configured + * and we should fall back to WebBrowser. + */ + private static shouldFallbackToWebBrowser(error: unknown): boolean { + if (error instanceof Error) { + const message = error.message.toLowerCase(); + // Common GoogleSignin setup errors that indicate fallback is needed + return ( + message.includes('developer_error') || + message.includes('sign_in_required') || + message.includes('play services') || + message.includes('not configured') || + message.includes('google sign in not available') + ); + } + return false; + } + + /** + * Configure GoogleSignin with the provided options. + */ + private static configureGoogleSignin( + nativeConfig: INativeOAuthConfig | undefined, + ): void { + const configOptions: Parameters[0] = { + scopes: nativeConfig?.scopes ?? ['openid', 'profile', 'email'], + offlineAccess: true, + }; + + if (platformEnv.isNativeIOS) { + configOptions.iosClientId = GOOGLE_OAUTH_CLIENT_IDS.IOS; + } + if (platformEnv.isNativeAndroid) { + configOptions.webClientId = GOOGLE_OAUTH_CLIENT_IDS.ANDROID; + } + + GoogleSignin.configure(configOptions); + } + + /** + * Open OAuth using @react-native-google-signin/google-signin. + * + * Flow: + * 1. Configure GoogleSignin with client IDs + * 2. Call GoogleSignin.signIn() for native Google Sign-In UI + * 3. Get Google ID token from result + * 4. Exchange ID token for Supabase session using signInWithIdToken + * 5. Handle session persistence + */ + private static async openWithGoogleSignin( + options: IOAuthPopupOptions, + ): Promise { + const { client, nativeConfig, handleSessionPersistence } = options; + + if (!client) { + throw new OneKeyLocalError('Supabase client is required'); + } + + // Configure GoogleSignin + OAuthPopup.configureGoogleSignin(nativeConfig); + + try { + // Check if Google Play Services is available (Android only) + if (platformEnv.isNativeAndroid) { + await GoogleSignin.hasPlayServices({ + showPlayServicesUpdateDialog: true, + }); + } + + // Perform Google Sign-In + // The signIn() method returns different types based on library version: + // - v9+: SignInResponse with { type: 'success' | 'cancelled', data?: User } + // - older: User directly + // We handle both cases for compatibility + const signInResult = await GoogleSignin.signIn(); + + // Extract idToken - handle both v9+ and older API + // v9+: signInResult may have .type and .data properties + // older: signInResult has .idToken directly + let idToken: string | null = null; + + // Type guard for v9+ API response + const resultWithType = signInResult as { + type?: string; + data?: { idToken?: string | null }; + idToken?: string | null; + }; + + if (resultWithType.type === 'cancelled') { + throw new OneKeyLocalError('OAuth sign-in was cancelled'); + } + + if (resultWithType.data?.idToken) { + // v9+ API: { type: 'success', data: { idToken: '...' } } + idToken = resultWithType.data.idToken; + } else if (resultWithType.idToken) { + // Older API: { idToken: '...' } + idToken = resultWithType.idToken; + } + + if (!idToken) { + throw new OneKeyLocalError( + 'No ID token received from Google Sign-In. ' + + 'Make sure webClientId is configured correctly.', + ); + } + + // Exchange Google ID token for Supabase session + // Per Supabase docs: https://supabase.com/docs/guides/auth/social-login/auth-google + const { data, error } = await client.auth.signInWithIdToken({ + provider: 'google', + token: idToken, + }); + + if (error) { + throw new OneKeyLocalError(error.message); + } + + if (!data.session) { + throw new OneKeyLocalError( + 'Failed to exchange Google ID token for session', + ); + } + + const accessToken = data.session.access_token; + const refreshToken = data.session.refresh_token; + + // Handle session persistence + await handleSessionPersistence({ + accessToken, + refreshToken, + }); + + return { + success: true, + session: { accessToken, refreshToken }, + }; + } catch (error) { + // Handle specific GoogleSignin errors + if (OAuthPopup.isUserCancelledError(error)) { + throw new OneKeyLocalError('OAuth sign-in was cancelled'); + } + throw OAuthPopup.wrapError(error, 'Google Sign-In failed'); + } + } + + // ============ Private Methods - WebBrowser ============ + + /** + * Open OAuth using expo-web-browser.openAuthSessionAsync. + * + * This is the fallback method that opens an in-app browser. + * Extracts tokens from the callback URL when authentication is complete. + */ + private static async openWithWebBrowser( + options: IOAuthPopupOptions, + ): Promise { + const { + authUrl, + redirectTo: redirectToFromOptions, + handleSessionPersistence, + } = options; + + if (!authUrl) { + throw new OneKeyLocalError('OAuth URL is required for WebBrowser method'); + } + + if (!redirectToFromOptions) { + throw new OneKeyLocalError( + 'redirectTo is required. Call OAuthPopup.getRedirectUrl() first.', + ); + } + + const redirectTo = redirectToFromOptions; + + // Open in-app browser for OAuth + const browserResult = await WebBrowser.openAuthSessionAsync( + authUrl, + redirectTo, + { + showInRecents: true, + preferEphemeralSession: false, + }, + ); + + if (browserResult.type === 'success' && browserResult.url) { + // Extract tokens from the callback URL + const { accessToken, refreshToken } = OAuthPopup.parseCallbackUrl( + browserResult.url, + ); + + if (accessToken && refreshToken) { + await handleSessionPersistence({ + accessToken, + refreshToken, + }); + + return { + success: true, + session: { accessToken, refreshToken }, + }; + } + } + + if (browserResult.type === 'cancel') { + throw new OneKeyLocalError('OAuth sign-in was cancelled'); + } + + throw new OneKeyLocalError('OAuth sign-in failed'); + } + + /** + * Parse tokens from callback URL (for WebBrowser method). + */ + private static parseCallbackUrl(url: string): { + accessToken: string | null; + refreshToken: string | null; + } { + const parsedUrl = new URL(url); + const hashParams = new URLSearchParams( + parsedUrl.hash.substring(1) || parsedUrl.search.substring(1), + ); + + return { + accessToken: hashParams.get(OAUTH_TOKEN_KEY_ACCESS_TOKEN), + refreshToken: hashParams.get(OAUTH_TOKEN_KEY_REFRESH_TOKEN), + }; + } +} diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.tsx b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.tsx new file mode 100644 index 000000000000..ec6965f8d5ff --- /dev/null +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.tsx @@ -0,0 +1,264 @@ +import { + OAUTH_CALLBACK_WEB_PATH, + OAUTH_FLOW_TIMEOUT_MS, + OAUTH_POLL_INTERVAL_MS, + OAUTH_POPUP_HEIGHT, + OAUTH_POPUP_WIDTH, + ONEKEY_OAUTH_STATE_KEY, +} from '@onekeyhq/shared/src/consts/authConsts'; +import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; + +import { OAuthPopupBase } from './OAuthPopupBase'; + +import type { IOAuthPopupOptions, IOAuthPopupResult } from './types'; + +// ============================================================================ +// Web OAuth Popup Implementation +// ============================================================================ + +/** + * OAuth popup implementation for web platform. + * + * Uses a popup window for OAuth authentication. + * Polls the popup URL to detect callback and extract authorization code. + * Uses Supabase PKCE flow for secure token exchange. + */ +export class OAuthPopup extends OAuthPopupBase { + // ============ Public API ============ + + /** + * Get OAuth redirect URL for web platform. + * + * Uses the current origin with /oauth_callback_web path. + * Example: https://app.onekey.so/oauth_callback_web + */ + static override getRedirectUrl(): Promise { + return Promise.resolve( + `${globalThis.location?.origin || ''}${OAUTH_CALLBACK_WEB_PATH}`, + ); + } + + /** + * Open OAuth popup window and return result. + * + * Flow: + * 1. Open popup window with OAuth URL + * 2. Poll popup URL for authorization code + * 3. Exchange code for session using Supabase PKCE + * 4. Handle session persistence + */ + static override async open( + options: IOAuthPopupOptions, + ): Promise { + const { authUrl, client, handleSessionPersistence } = options; + + if (!authUrl) { + throw new OneKeyLocalError('OAuth URL is required'); + } + + if (!client) { + throw new OneKeyLocalError('Supabase client is required'); + } + + return new Promise((resolve, reject) => { + let settled = false; + let inFlight = false; + let pollIntervalId: ReturnType | null = null; + let timeoutId: ReturnType | null = null; + + // Parse expected states for validation + const { expectedState, expectedOneKeyState } = + OAuthPopup.parseExpectedStates(authUrl); + + const cleanup = (popup: Window | null) => { + if (pollIntervalId) { + clearInterval(pollIntervalId); + pollIntervalId = null; + } + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + if (popup && !popup.closed) { + OAuthPopup.closePopup(popup); + } + }; + + const resolveOnce = (result: IOAuthPopupResult, popup: Window | null) => { + if (settled) { + return; + } + settled = true; + cleanup(popup); + resolve(result); + }; + + const rejectOnce = (error: unknown, popup: Window | null) => { + if (settled) { + return; + } + settled = true; + cleanup(popup); + reject(error); + }; + + // Calculate popup window position (centered) + const width = OAUTH_POPUP_WIDTH; + const height = OAUTH_POPUP_HEIGHT; + const left = globalThis.screenX + (globalThis.outerWidth - width) / 2; + const top = globalThis.screenY + (globalThis.outerHeight - height) / 2; + + // Open popup window + const popup = globalThis.open( + authUrl, + 'oauth_popup', + `width=${width},height=${height},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,scrollbars=yes,resizable=yes`, + ); + + if (!popup) { + rejectOnce( + new OneKeyLocalError( + 'Popup was blocked. Please allow popups and try again.', + ), + null, + ); + return; + } + + OAuthPopup.focusPopup(popup); + + // Poll for popup close and check for auth code (PKCE flow) + pollIntervalId = setInterval(() => { + if (inFlight) { + return; + } + inFlight = true; + + void (async () => { + try { + if (settled) { + return; + } + + OAuthPopup.focusPopup(popup); + + // Check if popup is closed + if (popup.closed) { + // Check if we got a session after popup closed + const { data } = await client.auth.getSession(); + if (data.session) { + const accessToken = data.session.access_token; + const refreshToken = data.session.refresh_token; + + await handleSessionPersistence({ + accessToken, + refreshToken, + }); + + resolveOnce( + { + success: true, + session: { accessToken, refreshToken }, + }, + popup, + ); + } else { + rejectOnce( + new OneKeyLocalError( + 'OAuth authentication failed: no session found after popup closed', + ), + popup, + ); + } + return; + } + + // Try to read the popup URL to check for callback + try { + const popupUrl = popup.location.href; + // PKCE flow: check for 'code' parameter in URL + if (popupUrl && popupUrl.includes('code=')) { + OAuthPopup.closePopup(popup); + + // Parse authorization code from URL query string + const url = new URL(popupUrl); + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + const oneKeyState = url.searchParams.get( + ONEKEY_OAUTH_STATE_KEY, + ); + + if (!code) { + rejectOnce( + new OneKeyLocalError('Authorization code is missing'), + popup, + ); + return; + } + + // Validate states + OAuthPopup.validateOneKeyState( + expectedOneKeyState, + oneKeyState, + ); + OAuthPopup.validateSupabaseState(expectedState, state); + + // Exchange code for session + const { accessToken, refreshToken } = + await OAuthPopup.exchangeCodeForSession(client, code); + + await handleSessionPersistence({ + accessToken, + refreshToken, + }); + + resolveOnce( + { + success: true, + session: { accessToken, refreshToken }, + }, + popup, + ); + } + } catch { + // Cross-origin error - popup is on different domain, continue polling + } + } catch (error) { + rejectOnce(error, popup); + } finally { + inFlight = false; + } + })(); + }, OAUTH_POLL_INTERVAL_MS); + + // Cleanup after timeout + timeoutId = setTimeout(() => { + rejectOnce(new OneKeyLocalError('OAuth sign-in timed out'), popup); + }, OAUTH_FLOW_TIMEOUT_MS); + }); + } + + // ============ Private Methods ============ + + /** + * Focus the popup window to bring it to front. + */ + private static focusPopup(win: Window | null): void { + try { + win?.focus(); + } catch { + // Focusing may fail, silently ignore + } + } + + /** + * Close the popup window safely. + */ + private static closePopup(win: Window | null): void { + try { + win?.close(); + } catch { + // Closing may fail, silently ignore + } + } +} diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopupBase.ts b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopupBase.ts new file mode 100644 index 000000000000..9b5c634d3ead --- /dev/null +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopupBase.ts @@ -0,0 +1,201 @@ +import { + OAUTH_FLOW_TIMEOUT_MS, + ONEKEY_OAUTH_STATE_KEY, +} from '@onekeyhq/shared/src/consts/authConsts'; +import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; + +import type { IOAuthPopupOptions, IOAuthPopupResult } from './types'; +import type { SupabaseClient } from '@supabase/supabase-js'; + +// ============================================================================ +// Parsed States Type +// ============================================================================ + +export interface IParsedOAuthStates { + // Supabase OAuth state (anti-CSRF) + expectedState: string | null; + // OneKey custom state (defense-in-depth) + expectedOneKeyState: string | null; +} + +// ============================================================================ +// Abstract Base Class +// ============================================================================ + +/** + * Abstract base class for OAuth popup implementations. + * + * Each platform (web, ext, desktop, native) extends this class + * and implements the abstract methods. + */ +export abstract class OAuthPopupBase { + // ============ Abstract Methods (platforms must implement) ============ + + /** + * Get the OAuth redirect URL for this platform. + * Some platforms return a Promise (e.g., desktop needs to start a server first). + */ + static getRedirectUrl(): Promise { + throw new OneKeyLocalError( + 'OAuthPopupBase.getRedirectUrl() must be implemented by platform', + ); + } + + /** + * Open OAuth popup and return result. + * Platform implementations handle the OAuth flow internally. + */ + static open(_options: IOAuthPopupOptions): Promise { + throw new OneKeyLocalError( + 'OAuthPopupBase.open() must be implemented by platform', + ); + } + + // ============ Protected Shared Utilities ============ + + /** + * Parse expected states from Supabase OAuth URL. + * + * Extracts: + * - Supabase state parameter (for CSRF protection) + * - OneKey custom state from redirect_to URL (defense-in-depth) + */ + protected static parseExpectedStates(authUrl: string): IParsedOAuthStates { + try { + const authUrlObj = new URL(authUrl); + const expectedState = authUrlObj.searchParams.get('state'); + + // Parse our own state from the embedded redirect_to URL + let expectedOneKeyState: string | null = null; + const redirectTo = authUrlObj.searchParams.get('redirect_to'); + if (redirectTo) { + try { + const redirectToUrl = new URL(redirectTo); + expectedOneKeyState = redirectToUrl.searchParams.get( + ONEKEY_OAUTH_STATE_KEY, + ); + } catch { + expectedOneKeyState = null; + } + } + + return { expectedState, expectedOneKeyState }; + } catch { + return { expectedState: null, expectedOneKeyState: null }; + } + } + + /** + * Validate OneKey custom state parameter (defense-in-depth). + * Throws if validation fails. + */ + protected static validateOneKeyState( + expectedState: string | null, + actualState: string | null, + ): void { + if (!expectedState) { + throw new OneKeyLocalError('Expected OneKey OAuth state is missing'); + } + if (!actualState) { + throw new OneKeyLocalError('OAuth state is missing'); + } + if (actualState !== expectedState) { + throw new OneKeyLocalError('OAuth state mismatch'); + } + } + + /** + * Validate Supabase OAuth state parameter (anti-CSRF). + * Only validates if expectedState is provided. + * Throws if validation fails. + */ + protected static validateSupabaseState( + expectedState: string | null, + actualState: string | null, + ): void { + if (expectedState) { + if (!actualState) { + throw new OneKeyLocalError('OAuth state is missing'); + } + if (actualState !== expectedState) { + throw new OneKeyLocalError('OAuth state mismatch'); + } + } + } + + /** + * Create a timeout promise that rejects after specified milliseconds. + */ + protected static createTimeoutPromise( + ms: number = OAUTH_FLOW_TIMEOUT_MS, + ): Promise { + return new Promise((_, reject) => { + setTimeout(() => { + reject(new OneKeyLocalError('OAuth sign-in timed out')); + }, ms); + }); + } + + /** + * Race a promise against timeout. + * Returns the promise result or throws timeout error. + */ + protected static async withTimeout( + promise: Promise, + ms: number = OAUTH_FLOW_TIMEOUT_MS, + ): Promise { + return Promise.race([promise, this.createTimeoutPromise(ms)]); + } + + /** + * Exchange authorization code for session using Supabase PKCE flow. + * The Supabase client automatically uses the stored code_verifier. + */ + protected static async exchangeCodeForSession( + client: SupabaseClient, + code: string, + ): Promise<{ accessToken: string; refreshToken: string }> { + const { data, error } = await client.auth.exchangeCodeForSession(code); + + if (error) { + throw new OneKeyLocalError(error.message); + } + + if (!data.session) { + throw new OneKeyLocalError( + 'Failed to exchange authorization code for session', + ); + } + + return { + accessToken: data.session.access_token, + refreshToken: data.session.refresh_token, + }; + } + + /** + * Wrap error with OneKeyLocalError if not already. + */ + protected static wrapError(error: unknown, fallbackMessage: string): Error { + if (error instanceof OneKeyLocalError) { + return error; + } + return new OneKeyLocalError( + error instanceof Error ? error.message : fallbackMessage, + ); + } + + /** + * Check if error indicates user cancelled OAuth. + */ + protected static isUserCancelledError(error: unknown): boolean { + if (error instanceof Error) { + return ( + error.message.includes('The user did not approve') || + error.message.includes('cancelled') || + error.message.includes('canceled') + ); + } + return false; + } +} diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/index.ts b/packages/kit/src/components/OneKeyAuth/OAuthPopup/index.ts new file mode 100644 index 000000000000..ec179b37ef4e --- /dev/null +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/index.ts @@ -0,0 +1,21 @@ +// Re-export platform-specific OAuthPopup class +// The bundler will automatically select the correct file based on platform: +// - OAuthPopup.web.tsx for web +// - OAuthPopup.ext.tsx for browser extension +// - OAuthPopup.desktop.tsx for desktop (Electron) +// - OAuthPopup.native.tsx for native (iOS/Android) + +export { OAuthPopup } from './OAuthPopup'; + +// Re-export types +export type { + IHandleOAuthSessionPersistenceParams, + INativeOAuthConfig, + IOAuthPopupOptions, + IOAuthPopupResult, + IOpenOAuthPopupOptions, +} from './types'; + +// Re-export base class for advanced use cases +export { OAuthPopupBase } from './OAuthPopupBase'; +export type { IParsedOAuthStates } from './OAuthPopupBase'; diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/types.ts b/packages/kit/src/components/OneKeyAuth/OAuthPopup/types.ts new file mode 100644 index 000000000000..866abb2a5ac9 --- /dev/null +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/types.ts @@ -0,0 +1,67 @@ +import type { SupabaseClient } from '@supabase/supabase-js'; + +// ============================================================================ +// Session Persistence Types +// ============================================================================ + +export type IHandleOAuthSessionPersistenceParams = { + accessToken: string; + refreshToken: string; +}; + +// ============================================================================ +// OAuth Result Types +// ============================================================================ + +export type IOAuthPopupResult = { + success: boolean; + session?: { + accessToken: string; + refreshToken: string; + }; +}; + +// ============================================================================ +// OAuth Options Types +// ============================================================================ + +export type IOpenOAuthPopupOptions = { + // Whether to persist the session to storage + // When false (default): Only return tokens, don't call setSession + persistSession?: boolean; +}; + +/** + * OAuth configuration for Google sign-in (native iOS/Android). + * These values should match your Google Cloud Console OAuth 2.0 Client ID settings. + */ +export interface INativeOAuthConfig { + // Google OAuth Client ID for iOS + // Create this in Google Cloud Console > APIs & Services > Credentials > OAuth 2.0 Client IDs + // Application type: iOS + iosClientId?: string; + // Google OAuth Client ID for Android (Web client ID is used for Android) + // Application type: Web application + webClientId?: string; + // OAuth scopes to request + scopes?: string[]; +} + +/** + * Unified OAuth popup options for all platforms + */ +export interface IOAuthPopupOptions { + // The OAuth authorization URL to open + authUrl?: string; + // The OAuth redirect URL (with onekey_oauth_state if needed) + // This should be the same URL passed to Supabase signInWithOAuth + redirectTo?: string; + // Supabase client instance (for code exchange) + client?: SupabaseClient; + // Function to handle session persistence after OAuth success + handleSessionPersistence: ( + params: IHandleOAuthSessionPersistenceParams, + ) => Promise; + // Native-specific OAuth config (only used on native platform) + nativeConfig?: INativeOAuthConfig; +} diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupTypes.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupTypes.tsx index 52de4d202a59..2fca076bc4d4 100644 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupTypes.tsx +++ b/packages/kit/src/components/OneKeyAuth/openOAuthPopupTypes.tsx @@ -1,9 +1,6 @@ export type IHandleOAuthSessionPersistenceParams = { accessToken: string; refreshToken: string; - persistSession?: boolean; - // Whether to also login to Prime service (default: true) - loginToPrime?: boolean; }; export type IOAuthPopupResult = { diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx index e44945b7d134..3aeebcff2bdf 100644 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx +++ b/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx @@ -182,8 +182,6 @@ export async function openOAuthPopupWeb(options: { await handleSessionPersistence({ accessToken, refreshToken, - persistSession, - loginToPrime: false, // openOAuthPopupWeb doesn't handle Prime login }); resolveOnce( @@ -291,8 +289,6 @@ export async function openOAuthPopupWeb(options: { await handleSessionPersistence({ accessToken, refreshToken, - persistSession, - loginToPrime: false, // openOAuthPopupWeb doesn't handle Prime login }); resolveOnce( diff --git a/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx b/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx index 0c22b0366c40..b5b3e5f0e901 100644 --- a/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx +++ b/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx @@ -4,38 +4,11 @@ import { useCallback, useMemo } from 'react'; import { useIntl } from 'react-intl'; import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; -import { - DEFAULT_DESKTOP_OAUTH_METHOD, - DEFAULT_EXTENSION_OAUTH_METHOD, - EDesktopOAuthMethod, - EExtensionOAuthMethod, - GOOGLE_CHROME_EXTENSION_CLIENT_ID, -} from '@onekeyhq/shared/src/consts/authConsts'; import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; import { ETranslations } from '@onekeyhq/shared/src/locale'; -import platformEnv from '@onekeyhq/shared/src/platformEnv'; +import { OAuthPopup } from '../OAuthPopup'; import { ensureOneKeyOAuthState } from '../oauthUtils'; -import { - getOAuthRedirectUrlDesktop, - openOAuthPopupDesktopDeepLink, - openOAuthPopupDesktopLocalhost, - openOAuthPopupDesktopWebview, -} from '../openOAuthPopupDesktop'; -import { - getOAuthRedirectUrlExt, - openOAuthPopupExtIdToken, - openOAuthPopupExtIdentity, - openOAuthPopupExtWindow, -} from '../openOAuthPopupExt'; -import { - getOAuthRedirectUrlNative, - openOAuthPopupNative, -} from '../openOAuthPopupNative'; -import { - getOAuthRedirectUrlWeb, - openOAuthPopupWeb, -} from '../openOAuthPopupWeb'; import { createTemporarySupabaseClient, @@ -45,36 +18,6 @@ import { useSupabaseAuthContext } from './SupabaseAuthContext'; import type { AuthResponse, SupabaseClient } from '@supabase/supabase-js'; -// Helper function to handle OAuth session persistence -// This function is called after successfully extracting tokens from OAuth callback -async function handleOAuthSessionPersistence({ - accessToken, - refreshToken, - persistSession, - loginToPrime, -}: { - accessToken: string; - refreshToken: string; - persistSession?: boolean; - // Whether to also login to Prime service - loginToPrime?: boolean; -}): Promise { - if (persistSession) { - // Persist session to Supabase client storage - await getSupabaseClient().client.auth.setSession({ - access_token: accessToken, - refresh_token: refreshToken, - }); - - // Login to Prime service - if (loginToPrime) { - await backgroundApiProxy.servicePrime.apiLogin({ - accessToken, - }); - } - } -} - export function useSupabaseAuth() { const ctx = useSupabaseAuthContext(); const supabaseUser = ctx?.session?.user; @@ -105,37 +48,34 @@ export function useSupabaseAuth() { const { persistSession } = options ?? {}; const clientTemp: SupabaseClient = createTemporarySupabaseClient(); - // For extension with CHROME_IDENTITY_API or CHROME_GET_AUTH_TOKEN methods, - // we don't need Supabase OAuth URL - these methods build their own Google OAuth URL - // and use signInWithIdToken instead - if (platformEnv.isExtension) { - if ( - DEFAULT_EXTENSION_OAUTH_METHOD === - EExtensionOAuthMethod.CHROME_GET_AUTH_TOKEN - ) { - // Use getAuthToken (requires manifest oauth2 config) - // Chrome handles OAuth internally, no redirect URL needed - return openOAuthPopupExtIdToken({ - handleSessionPersistence: handleOAuthSessionPersistence, - persistSession, + const handleOAuthSessionPersistence = async ({ + accessToken, + refreshToken, + }: { + accessToken: string; + refreshToken: string; + }): Promise => { + if (persistSession) { + // Persist session to Supabase client storage + await getSupabaseClient().client.auth.setSession({ + access_token: accessToken, + refresh_token: refreshToken, }); + + // Login to Prime service + // if (loginToPrime) { + // await backgroundApiProxy.servicePrime.apiLogin({ + // accessToken, + // }); + // } } - } + }; - // For other platforms and DIRECT_EXTENSION_SCHEME, we need Supabase OAuth URL - // Build redirect URL based on platform - let redirectTo: string | undefined; - if (platformEnv.isNative) { - redirectTo = getOAuthRedirectUrlNative(); - } else if (platformEnv.isDesktop) { - redirectTo = await getOAuthRedirectUrlDesktop( - DEFAULT_DESKTOP_OAUTH_METHOD, - ); - } else if (platformEnv.isExtension) { - redirectTo = getOAuthRedirectUrlExt(DEFAULT_EXTENSION_OAUTH_METHOD); - } else { - redirectTo = getOAuthRedirectUrlWeb(); - } + // Get platform-specific redirect URL + // Note: Some platforms return Promise (e.g., desktop needs to start server) + let redirectTo: string | undefined = await Promise.resolve( + OAuthPopup.getRedirectUrl(), + ); // Defense-in-depth: Supabase PKCE URL may not include `state`. We embed our own // nonce into redirectTo so the callback must carry it back to us. @@ -143,6 +83,7 @@ export function useSupabaseAuth() { redirectTo = ensureOneKeyOAuthState(redirectTo); } + // Get Supabase OAuth URL const oauthUrlResult = await clientTemp.auth.signInWithOAuth({ provider, options: { @@ -167,73 +108,13 @@ export function useSupabaseAuth() { throw new OneKeyLocalError('Failed to get OAuth URL'); } - // Open the OAuth URL based on platform - if (platformEnv.isNative) { - return openOAuthPopupNative({ - authUrl, - redirectTo, - handleSessionPersistence: handleOAuthSessionPersistence, - persistSession, - }); - } - - // For desktop (Electron), handle OAuth based on configured method - if (platformEnv.isDesktop) { - if ( - DEFAULT_DESKTOP_OAUTH_METHOD === EDesktopOAuthMethod.LOCALHOST_SERVER - ) { - return openOAuthPopupDesktopLocalhost({ - authUrl, - client: clientTemp, - handleSessionPersistence: handleOAuthSessionPersistence, - persistSession, - }); - } - if (DEFAULT_DESKTOP_OAUTH_METHOD === EDesktopOAuthMethod.WEBVIEW) { - return openOAuthPopupDesktopWebview({ - authUrl, - handleSessionPersistence: handleOAuthSessionPersistence, - persistSession, - }); - } - return openOAuthPopupDesktopDeepLink({ - authUrl, - handleSessionPersistence: handleOAuthSessionPersistence, - persistSession, - }); - } - - // For extension with DIRECT_EXTENSION_SCHEME (does not work, kept for reference) - if (platformEnv.isExtension) { - if ( - DEFAULT_EXTENSION_OAUTH_METHOD === - EExtensionOAuthMethod.CHROME_IDENTITY_API - ) { - // Use launchWebAuthFlow + signInWithIdToken (Supabase recommended) - // Pass authUrl from external creation (similar to web version) - return openOAuthPopupExtIdentity({ - client: clientTemp, - config: { googleClientId: GOOGLE_CHROME_EXTENSION_CLIENT_ID }, - handleSessionPersistence: handleOAuthSessionPersistence, - persistSession, - authUrl, - }); - } - return openOAuthPopupExtWindow({ - authUrl, - handleSessionPersistence: handleOAuthSessionPersistence, - persistSession, - }); - } - - // Open OAuth popup window for web - const popupResult = await openOAuthPopupWeb({ + // Open OAuth popup using platform-specific implementation + return OAuthPopup.open({ authUrl, + redirectTo, client: clientTemp, handleSessionPersistence: handleOAuthSessionPersistence, - persistSession, }); - return popupResult; }, [], ); diff --git a/packages/shared/src/consts/authConsts.ts b/packages/shared/src/consts/authConsts.ts index 5df4e9336fdd..17eacc7ac885 100644 --- a/packages/shared/src/consts/authConsts.ts +++ b/packages/shared/src/consts/authConsts.ts @@ -59,6 +59,18 @@ export enum EExtensionOAuthMethod { DIRECT_EXTENSION_SCHEME = 'DIRECT_EXTENSION_SCHEME', } +export enum ENativeOAuthMethod { + // ✅ RECOMMENDED: Use @react-native-google-signin/google-signin with signInWithIdToken + // Uses native Google Sign-In UI for better UX + // Gets Google ID token and exchanges it for Supabase session + GOOGLE_SIGNIN = 'GOOGLE_SIGNIN', + + // Fallback: Use expo-web-browser.openAuthSessionAsync + // Opens in-app browser for OAuth, uses deep link callback + // Redirect URL: onekey-wallet://auth/callback + WEB_BROWSER = 'WEB_BROWSER', +} + // 5 minutes OAuth timeout (used by web/desktop/ext flows) export const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000; @@ -106,6 +118,8 @@ export const DEFAULT_EXTENSION_OAUTH_METHOD: EExtensionOAuthMethod = EExtensionOAuthMethod.CHROME_IDENTITY_API; export const DEFAULT_DESKTOP_OAUTH_METHOD: EDesktopOAuthMethod = EDesktopOAuthMethod.LOCALHOST_SERVER; +export const DEFAULT_NATIVE_OAUTH_METHOD: ENativeOAuthMethod = + ENativeOAuthMethod.GOOGLE_SIGNIN; // Google OAuth clients // - https://console.cloud.google.com/auth/clients @@ -114,10 +128,18 @@ export const DEFAULT_DESKTOP_OAUTH_METHOD: EDesktopOAuthMethod = // - OAuth client: https://console.cloud.google.com/apis/credentials // - Authorized redirect URIs (for chrome.identity.launchWebAuthFlow): // https://.chromiumapp.org (no trailing slash) -export const GOOGLE_CHROME_EXTENSION_CLIENT_ID = - '244450898872-d22ubafv8ca38s6fp0kflhdr6e3s386u.apps.googleusercontent.com'; // oauth web client, not extension client + // TODO: Search for all occurrences of 'apps.googleusercontent.com' in the project and consolidate all discovered OAuth client IDs here for unified management. +const GOOGLE_OAUTH_CLIENT_WEB = + '244450898872-d22ubafv8ca38s6fp0kflhdr6e3s386u.apps.googleusercontent.com'; +export const GOOGLE_OAUTH_CLIENT_IDS = { + WEB: GOOGLE_OAUTH_CLIENT_WEB, + EXTENSION: GOOGLE_OAUTH_CLIENT_WEB, // oauth web client, not extension client + ANDROID: GOOGLE_OAUTH_CLIENT_WEB, + IOS: '244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt.apps.googleusercontent.com', +}; + // Supabase (OneKeyAuth) // Project URL at https://supabase.com/dashboard/project/_/settings/api export const SUPABASE_PROJECT_URL = IS_DEV From a5d3d0564790d85aba60425328fb6154b90c13bd Mon Sep 17 00:00:00 2001 From: morizon Date: Mon, 22 Dec 2025 16:53:41 +0800 Subject: [PATCH 10/66] refactor: remove obsolete OAuth components and types - Deleted unused OAuth handling files for desktop, extension, native, and web platforms to streamline the codebase. - Removed related types and utility functions that are no longer necessary, enhancing maintainability and clarity. - This cleanup aligns with recent refactoring efforts to consolidate OAuth handling across platforms. --- .../OneKeyAuth/openOAuthPopupDesktop.tsx | 669 --------------- .../OneKeyAuth/openOAuthPopupExt.tsx | 810 ------------------ .../OneKeyAuth/openOAuthPopupNative.tsx | 94 -- .../OneKeyAuth/openOAuthPopupTypes.tsx | 31 - .../OneKeyAuth/openOAuthPopupWeb.tsx | 320 ------- 5 files changed, 1924 deletions(-) delete mode 100644 packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx delete mode 100644 packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx delete mode 100644 packages/kit/src/components/OneKeyAuth/openOAuthPopupNative.tsx delete mode 100644 packages/kit/src/components/OneKeyAuth/openOAuthPopupTypes.tsx delete mode 100644 packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx deleted file mode 100644 index dacf40f4ab0b..000000000000 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupDesktop.tsx +++ /dev/null @@ -1,669 +0,0 @@ -import { Dialog } from '@onekeyhq/components'; -import type { IDialogInstance } from '@onekeyhq/components'; -import type { IDesktopOpenUrlEventData } from '@onekeyhq/desktop/app/app'; -import { ipcMessageKeys } from '@onekeyhq/desktop/app/config'; -import { - EDesktopOAuthMethod, - OAUTH_CALLBACK_DESKTOP_CHANNEL, - OAUTH_CALLBACK_DESKTOP_PATH, - OAUTH_DESKTOP_WEBVIEW_HEIGHT, - OAUTH_DESKTOP_WEBVIEW_WIDTH, - OAUTH_FLOW_TIMEOUT_MS, - OAUTH_TOKEN_KEY_ACCESS_TOKEN, - OAUTH_TOKEN_KEY_REFRESH_TOKEN, - ONEKEY_OAUTH_STATE_KEY, -} from '@onekeyhq/shared/src/consts/authConsts'; -import { ONEKEY_APP_DEEP_LINK } from '@onekeyhq/shared/src/consts/deeplinkConsts'; -import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; -import platformEnv from '@onekeyhq/shared/src/platformEnv'; - -import type { - IHandleOAuthSessionPersistenceParams, - IOAuthPopupResult, -} from './openOAuthPopupTypes'; -import type { SupabaseClient } from '@supabase/supabase-js'; - -// ============================================================================ -// Desktop OAuth Methods -// ============================================================================ - -/** - * OAuth helper for Desktop (Electron) platform using localhost HTTP server - * with Supabase OAuth redirecting back to localhost (system-assigned port). - * - * This method uses Supabase as the OAuth intermediary with PKCE flow: - * Google -> Supabase -> localhost callback (authorization code in URL query) - * - * How it works: - * 1. Main process starts a localhost HTTP server on a system-assigned port (listen(0)) - * 2. Renderer opens Supabase OAuth URL in system browser (skipBrowserRedirect=true) - * 3. Supabase handles Google OAuth and redirects back with authorization code - * 4. Main process extracts code from URL query and sends it to renderer via IPC - * 5. Renderer exchanges code for session tokens using Supabase client - * 6. Renderer persists session via handleSessionPersistence - * - * Supabase Configuration: - * - Redirect URL is dynamically constructed with the assigned port - * - * @param options - Configuration options - * @param options.authUrl - Supabase OAuth URL (skipBrowserRedirect=true) - * @param options.client - Supabase client instance for exchanging code - * @param options.handleSessionPersistence - Function to handle session persistence - * @param options.persistSession - Whether to persist the session - * @returns Promise with success status and session tokens - */ -export async function openOAuthPopupDesktopLocalhost(options: { - authUrl: string; - client: SupabaseClient; - handleSessionPersistence: ( - params: IHandleOAuthSessionPersistenceParams, - ) => Promise; - persistSession?: boolean; -}): Promise { - const { authUrl, client, handleSessionPersistence, persistSession } = options; - - // Check if desktopApiProxy is available - if (!platformEnv.isDesktop || !globalThis.desktopApiProxy?.oauthLocalServer) { - throw new OneKeyLocalError( - 'Desktop OAuth Local Server API is not available', - ); - } - - return new Promise((resolve, reject) => { - void (async () => { - try { - let settled = false; - let timeoutId: ReturnType | null = null; - let dialogClosed = false; - let waitingDialog: IDialogInstance | null = null; - let expectedState: string | null = null; - let expectedOneKeyState: string | null = null; - - try { - const authUrlObj = new URL(authUrl); - expectedState = authUrlObj.searchParams.get('state'); - - const redirectTo = authUrlObj.searchParams.get('redirect_to'); - if (redirectTo) { - try { - expectedOneKeyState = new URL(redirectTo).searchParams.get( - ONEKEY_OAUTH_STATE_KEY, - ); - } catch { - expectedOneKeyState = null; - } - } - } catch { - expectedState = null; - expectedOneKeyState = null; - } - - const cleanupFn = { - cleanup: async () => {}, - }; - - // Listen for callback with authorization code via IPC (PKCE flow) - const handleCallback = async ( - _event: Electron.IpcRendererEvent, - data: { - code?: string; - state?: string; - oneKeyState?: string; - }, - ) => { - if (settled) { - return; - } - settled = true; - // Remove listener using desktopApi (for IPC events) - if (globalThis.desktopApi) { - globalThis.desktopApi.removeIpcEventListener( - OAUTH_CALLBACK_DESKTOP_CHANNEL, - handleCallback, - ); - } - - try { - dialogClosed = true; - await Promise.resolve(waitingDialog?.close()); - const code = data.code; - const state = data.state; - const oneKeyState = data.oneKeyState; - - if (!code) { - await cleanupFn.cleanup(); - reject(new OneKeyLocalError('Authorization code is missing')); - return; - } - - if (!expectedOneKeyState) { - await cleanupFn.cleanup(); - reject( - new OneKeyLocalError('Expected OneKey OAuth state is missing'), - ); - return; - } - - // Validate OneKey state (defense-in-depth). - if (!oneKeyState) { - await cleanupFn.cleanup(); - reject(new OneKeyLocalError('OAuth state is missing')); - return; - } - - if (oneKeyState !== expectedOneKeyState) { - await cleanupFn.cleanup(); - reject(new OneKeyLocalError('OAuth state mismatch')); - return; - } - - // Validate state (anti-CSRF / anti-injection). This does not change the redirect URI. - // Supabase OAuth URLs should include `state=...` and the redirect callback should echo it back. - if (expectedState) { - if (!state) { - await cleanupFn.cleanup(); - reject(new OneKeyLocalError('OAuth state is missing')); - return; - } - if (state !== expectedState) { - await cleanupFn.cleanup(); - reject(new OneKeyLocalError('OAuth state mismatch')); - return; - } - } - - // Exchange authorization code for session tokens using PKCE - // The Supabase client automatically uses the stored code_verifier - const { data: exchangeData, error } = - await client.auth.exchangeCodeForSession(code); - - if (error) { - await cleanupFn.cleanup(); - reject(new OneKeyLocalError(error.message)); - return; - } - - const session = exchangeData.session; - if (!session) { - await cleanupFn.cleanup(); - reject( - new OneKeyLocalError( - 'Failed to exchange authorization code for session', - ), - ); - return; - } - - const accessToken = session.access_token; - const refreshToken = session.refresh_token; - - // Handle session persistence - await handleSessionPersistence({ - accessToken, - refreshToken, - persistSession, - }); - - await cleanupFn.cleanup(); - resolve({ - success: true, - session: { accessToken, refreshToken }, - }); - } catch (error) { - await cleanupFn.cleanup(); - reject( - new OneKeyLocalError( - error instanceof Error ? error.message : 'OAuth failed', - ), - ); - } - }; - - cleanupFn.cleanup = async () => { - if (timeoutId) { - clearTimeout(timeoutId); - timeoutId = null; - } - if (globalThis.desktopApi) { - globalThis.desktopApi.removeIpcEventListener( - OAUTH_CALLBACK_DESKTOP_CHANNEL, - handleCallback, - ); - } - try { - await globalThis.desktopApiProxy.oauthLocalServer.stopServer(); - } catch { - // Ignore stop errors. - } - try { - if (!dialogClosed) { - await Promise.resolve(waitingDialog?.close()); - } - } catch { - // Ignore close errors. - } - }; - - // Show an in-app "waiting" dialog so users can cancel immediately. - // Note: When opening **external system browsers**, we cannot reliably detect - // whether the browser window/tab was closed. Cancel is the only reliable way. - waitingDialog = Dialog.show({ - title: 'Sign in', - description: - 'Complete sign-in in your browser, then return to OneKey.', - showFooter: true, - showConfirmButton: false, - showCancelButton: true, - onCancel: async (close) => { - if (settled) { - await close(); - return; - } - settled = true; - dialogClosed = true; - await close(); - await cleanupFn.cleanup(); - reject(new OneKeyLocalError('OAuth sign-in was cancelled')); - }, - onClose: async (extra) => { - // Treat manual dialog dismissal as cancel. - if (extra?.flag === 'cancel' && !settled) { - settled = true; - dialogClosed = true; - await cleanupFn.cleanup(); - reject(new OneKeyLocalError('OAuth sign-in was cancelled')); - } - }, - }); - - // Add listener using desktopApi (for IPC events) - if (globalThis.desktopApi) { - globalThis.desktopApi.addIpcEventListener( - OAUTH_CALLBACK_DESKTOP_CHANNEL, - handleCallback, - ); - } - - // Open Supabase OAuth in system browser - await globalThis.desktopApiProxy.oauthLocalServer.openBrowser(authUrl); - - // Timeout after 5 minutes - timeoutId = setTimeout(() => { - if (settled) { - return; - } - settled = true; - void cleanupFn.cleanup().finally(() => { - reject(new OneKeyLocalError('OAuth sign-in timed out')); - }); - }, OAUTH_FLOW_TIMEOUT_MS); - } catch (error) { - Dialog.debugMessage({ - title: 'OAuth', - debugMessage: - error instanceof Error ? error.message : 'OAuth setup failed', - }); - reject( - new OneKeyLocalError( - error instanceof Error ? error.message : 'OAuth setup failed', - ), - ); - } - })(); - }); -} - -/** - * Get OAuth redirect URL for desktop platform (Electron) - * - * Both WEBVIEW and DEEP_LINK methods use the same deep link scheme. - * - WEBVIEW: The webview intercepts navigation to this URL before it actually navigates - * - DEEP_LINK: The system handles this URL via registered protocol - * - * Desktop registers onekey-wallet:// via app.setAsDefaultProtocolClient() in apps/desktop/app/app.ts - * - * @param _method - The desktop OAuth method (currently both methods use the same URL) - * @returns The redirect URL for desktop OAuth - */ -export async function getOAuthRedirectUrlDesktop( - method: EDesktopOAuthMethod, -): Promise { - // Desktop LOCALHOST: use Supabase OAuth (skipBrowserRedirect) and a localhost callback. - // Flow: Google -> Supabase -> localhost (`code` in query, PKCE) -> app persists session. - if (method === EDesktopOAuthMethod.LOCALHOST_SERVER) { - if (!globalThis.desktopApiProxy?.oauthLocalServer) { - throw new OneKeyLocalError( - 'Desktop OAuth Local Server API is not available', - ); - } - let port = 0; - try { - const serverResult = - await globalThis.desktopApiProxy.oauthLocalServer.startServer(); - port = serverResult.port; - } catch (e) { - throw new OneKeyLocalError( - 'Failed to start OAuth local server. Please try again.', - ); - } - if (!port) { - throw new OneKeyLocalError('OAuth local server returned invalid port.'); - } - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `http://127.0.0.1:${port}${OAUTH_CALLBACK_DESKTOP_PATH}`; - } - - // Both WEBVIEW and DEEP_LINK methods use the same deep link URL - // The difference is how the URL is handled: - // - WEBVIEW: Intercepted by webview navigation event - // - DEEP_LINK: Handled by system protocol registration - return `${ONEKEY_APP_DEEP_LINK}auth/callback`; -} - -/** - * OAuth webview helper for Desktop (Electron) platform - WEBVIEW method - * - * Opens OAuth in an in-app webview dialog and intercepts the redirect. - * The webview monitors navigation and extracts tokens when the URL - * matches our redirect pattern (onekey-wallet://auth/callback) - * - * Pros: - * - Better UX - OAuth happens within the app - * - No need for system deep link registration - * - More reliable token extraction - * - * @param options - Configuration options - * @param options.authUrl - The OAuth authorization URL to open - * @param options.handleSessionPersistence - Function to handle session persistence - * @param options.persistSession - Whether to persist the session - * @returns Promise with success status and session tokens - */ -export function openOAuthPopupDesktopWebview(options: { - authUrl: string; - handleSessionPersistence: ( - params: IHandleOAuthSessionPersistenceParams, - ) => Promise; - persistSession?: boolean; -}): Promise { - const { authUrl, handleSessionPersistence, persistSession } = options; - - return new Promise((resolve, reject) => { - // Create a container for the OAuth webview - const container = document.createElement('div'); - container.id = 'oauth-webview-container'; - container.style.cssText = ` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 99999; - `; - - // Create webview wrapper - const wrapper = document.createElement('div'); - wrapper.style.cssText = ` - width: ${OAUTH_DESKTOP_WEBVIEW_WIDTH}px; - height: ${OAUTH_DESKTOP_WEBVIEW_HEIGHT}px; - background: white; - border-radius: 12px; - overflow: hidden; - box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); - display: flex; - flex-direction: column; - `; - - // Create header with close button - const header = document.createElement('div'); - header.style.cssText = ` - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; - border-bottom: 1px solid #e5e5e5; - background: #f5f5f5; - `; - - const title = document.createElement('span'); - title.textContent = 'Sign in'; - title.style.cssText = 'font-weight: 600; font-size: 14px;'; - - const closeButton = document.createElement('button'); - closeButton.textContent = '✕'; - closeButton.style.cssText = ` - border: none; - background: none; - font-size: 18px; - cursor: pointer; - padding: 4px 8px; - border-radius: 4px; - `; - closeButton.onmouseover = () => { - closeButton.style.background = '#e0e0e0'; - }; - closeButton.onmouseout = () => { - closeButton.style.background = 'none'; - }; - closeButton.onclick = () => { - container.remove(); - reject(new OneKeyLocalError('OAuth sign-in was cancelled')); - }; - - header.appendChild(title); - header.appendChild(closeButton); - - // Create webview element - const webview = document.createElement('webview'); - webview.setAttribute('src', authUrl); - webview.setAttribute('partition', 'persist:onekey-oauth'); - webview.style.cssText = 'flex: 1; width: 100%;'; - - // Handle navigation events to intercept OAuth callback - const handleDidStartNavigation = async (event: Event) => { - const navEvent = event as unknown as { - url: string; - isMainFrame: boolean; - }; - const { url: navUrl, isMainFrame } = navEvent; - - // Check if this is our OAuth callback - if ( - isMainFrame && - navUrl?.startsWith(`${ONEKEY_APP_DEEP_LINK}auth/callback`) - ) { - // Stop loading - we can't actually navigate to onekey-wallet:// - (webview as unknown as { stop: () => void }).stop?.(); - - // Remove the container - container.remove(); - - try { - // Parse tokens from the callback URL - const parsedUrl = new URL(navUrl); - const hashParams = new URLSearchParams( - parsedUrl.hash.substring(1) || parsedUrl.search.substring(1), - ); - - const accessToken = hashParams.get(OAUTH_TOKEN_KEY_ACCESS_TOKEN); - const refreshToken = hashParams.get(OAUTH_TOKEN_KEY_REFRESH_TOKEN); - - if (accessToken && refreshToken) { - await handleSessionPersistence({ - accessToken, - refreshToken, - persistSession, - }); - - resolve({ - success: true, - session: { - accessToken, - refreshToken, - }, - }); - } else { - reject( - new OneKeyLocalError( - 'OAuth authentication failed: access token or refresh token is missing', - ), - ); - } - } catch (error) { - reject(error); - } - } - }; - - webview.addEventListener('did-start-navigation', handleDidStartNavigation); - - // Handle webview load errors (e.g., if OAuth page fails to load) - webview.addEventListener('did-fail-load', (event: Event) => { - const failEvent = event as unknown as { - errorCode: number; - errorDescription: string; - validatedURL: string; - isMainFrame: boolean; - }; - // Ignore aborted loads (e.g., when we stop navigation to callback URL) - if (failEvent.errorCode === -3) { - return; - } - // Only handle main frame errors - if (failEvent.isMainFrame) { - container.remove(); - reject( - new OneKeyLocalError( - `OAuth page failed to load: ${failEvent.errorDescription}`, - ), - ); - } - }); - - // Assemble the UI - wrapper.appendChild(header); - wrapper.appendChild(webview); - container.appendChild(wrapper); - document.body.appendChild(container); - - // Click outside to close - container.onclick = (e) => { - if (e.target === container) { - container.remove(); - reject(new OneKeyLocalError('OAuth sign-in was cancelled')); - } - }; - - // Set a timeout - setTimeout(() => { - if (document.body.contains(container)) { - container.remove(); - reject(new OneKeyLocalError('OAuth sign-in timed out')); - } - }, OAUTH_FLOW_TIMEOUT_MS); // 5 minutes timeout - }); -} - -/** - * OAuth helper for Desktop (Electron) platform - DEEP_LINK method - * - * Opens OAuth URL in system browser and listens for deep link callback. - * Requires onekey-wallet:// protocol to be registered with the system. - * - * How it works: - * 1. Opens OAuth URL in system browser via shell.openExternal - * 2. User completes OAuth in browser - * 3. Browser redirects to onekey-wallet://auth/callback?tokens... - * 4. System routes this URL to our Electron app - * 5. App receives the URL via IPC and extracts tokens - * - * @param options - Configuration options - * @param options.authUrl - The OAuth authorization URL to open - * @param options.handleSessionPersistence - Function to handle session persistence - * @param options.persistSession - Whether to persist the session - * @returns Promise with success status and session tokens - */ -export function openOAuthPopupDesktopDeepLink(options: { - authUrl: string; - handleSessionPersistence: ( - params: IHandleOAuthSessionPersistenceParams, - ) => Promise; - persistSession?: boolean; -}): Promise { - const { authUrl, handleSessionPersistence, persistSession } = options; - - return new Promise((resolve, reject) => { - // Set up deep link listener for OAuth callback - const handleOAuthCallback = async ( - _event: Event, - data: IDesktopOpenUrlEventData, - ) => { - const { url } = data; - // Check if this is our OAuth callback - if (url?.startsWith(`${ONEKEY_APP_DEEP_LINK}auth/callback`)) { - // Remove listener once we got the callback - globalThis.desktopApi.removeIpcEventListener( - ipcMessageKeys.EVENT_OPEN_URL, - handleOAuthCallback, - ); - - try { - // Parse tokens from the callback URL - const parsedUrl = new URL(url); - const hashParams = new URLSearchParams( - parsedUrl.hash.substring(1) || parsedUrl.search.substring(1), - ); - - const accessToken = hashParams.get(OAUTH_TOKEN_KEY_ACCESS_TOKEN); - const refreshToken = hashParams.get(OAUTH_TOKEN_KEY_REFRESH_TOKEN); - - if (accessToken && refreshToken) { - await handleSessionPersistence({ - accessToken, - refreshToken, - persistSession, - }); - - resolve({ - success: true, - session: { - accessToken, - refreshToken, - }, - }); - } else { - reject( - new OneKeyLocalError( - 'OAuth authentication failed: access token or refresh token is missing', - ), - ); - } - } catch (error) { - reject(error); - } - } - }; - - // Add the listener - globalThis.desktopApi.addIpcEventListener( - ipcMessageKeys.EVENT_OPEN_URL, - handleOAuthCallback, - ); - - // Open OAuth URL in system browser - // On Electron, window.open with _blank target is intercepted and opens via shell.openExternal - window.open(authUrl, '_blank'); - - // Set a timeout to clean up listener if OAuth takes too long - setTimeout(() => { - globalThis.desktopApi.removeIpcEventListener( - ipcMessageKeys.EVENT_OPEN_URL, - handleOAuthCallback, - ); - reject(new OneKeyLocalError('OAuth sign-in timed out')); - }, OAUTH_FLOW_TIMEOUT_MS); // 5 minutes timeout - }); -} diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx deleted file mode 100644 index e9d87e5b607a..000000000000 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupExt.tsx +++ /dev/null @@ -1,810 +0,0 @@ -/* eslint-disable spellcheck/spell-checker */ -import { - EExtensionOAuthMethod, - EXTENSION_OAUTH_USE_PKCE_FLOW, - GOOGLE_OAUTH_AUTHORIZE_URL, - GOOGLE_OAUTH_DEFAULT_SCOPES, - GOOGLE_OAUTH_TOKENINFO_URL, - GOOGLE_OAUTH_USERINFO_URL, - OAUTH_FLOW_TIMEOUT_MS, - OAUTH_POLL_INTERVAL_MS, - OAUTH_POPUP_HEIGHT, - OAUTH_POPUP_WIDTH, - OAUTH_TOKEN_KEY_ACCESS_TOKEN, - OAUTH_TOKEN_KEY_ID_TOKEN, - OAUTH_TOKEN_KEY_REFRESH_TOKEN, - ONEKEY_OAUTH_STATE_KEY, -} from '@onekeyhq/shared/src/consts/authConsts'; -import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; - -import { ensureOneKeyOAuthState } from './oauthUtils'; - -import type { - IExtensionOAuthConfig, - IHandleOAuthSessionPersistenceParams, - IOAuthPopupResult, -} from './openOAuthPopupTypes'; -import type { SupabaseClient } from '@supabase/supabase-js'; - -// ============================================================================ -// Extension OAuth Methods -// ============================================================================ -const getRedirectUrl = (): string => { - // https://.chromiumapp.org/ - let redirectUrl = chrome.identity.getRedirectURL(); - // Remove trailing slash to match Google Cloud Console configuration (and existing behavior). - if (redirectUrl.endsWith('/')) { - redirectUrl = redirectUrl.slice(0, -1); - } - return redirectUrl; -}; -/** - * Get OAuth redirect URL for Chrome Extension - * - * Returns different URLs based on the OAuth method: - * - CHROME_IDENTITY_API: undefined (handled internally by openOAuthPopupExtIdentity) - * - CHROME_GET_AUTH_TOKEN: undefined (Chrome handles internally) - * - DIRECT_EXTENSION_SCHEME: chrome-extension:///ui-oauth-callback.html - * - * @param method - The extension OAuth method to use - * @returns The redirect URL for extension OAuth, or undefined if not needed - */ -export function getOAuthRedirectUrlExt( - method: EExtensionOAuthMethod, -): string | undefined { - if ( - method === EExtensionOAuthMethod.CHROME_IDENTITY_API || - method === EExtensionOAuthMethod.CHROME_GET_AUTH_TOKEN - ) { - return getRedirectUrl(); - } - // Use direct chrome-extension:// scheme - // Format: chrome-extension:///ui-oauth-callback.html - return chrome.runtime.getURL('ui-oauth-callback.html'); -} - -/** - * OAuth configuration for Google sign-in - * These values should match your Google Cloud Console OAuth 2.0 Client ID settings - */ -/** - * OAuth helper for Chrome Extension using getChromeApi().identity.launchWebAuthFlow - * with Google ID Token + Supabase signInWithIdToken - * - * This is the RECOMMENDED method for extension OAuth based on Supabase documentation. - * - * How it works: - * 1. Manually builds Google OAuth URL with response_type=id_token - * 2. Opens a popup window for OAuth using getChromeApi().identity.launchWebAuthFlow - * 3. Chrome handles the OAuth flow and redirect automatically - * 4. Extracts id_token from callback URL hash - * 5. Uses Supabase signInWithIdToken to exchange for session - * - * Supabase Configuration Required: - * - Add Chrome Extension Client ID to Supabase Dashboard > Authentication > Providers > Google - * - If you have multiple client IDs, concatenate them with comma (web ID first) - * - * @param options - Configuration options - * @param options.config - OAuth configuration including Google Client ID - * @param options.handleSessionPersistence - Function to handle session persistence - * @param options.persistSession - Whether to persist the session - * @returns Promise with success status and session tokens - */ -export async function openOAuthPopupExtIdentity(options: { - client: SupabaseClient; - config: IExtensionOAuthConfig; - handleSessionPersistence: ( - params: IHandleOAuthSessionPersistenceParams, - ) => Promise; - persistSession?: boolean; - authUrl?: string; -}): Promise { - const { - client, - config, - handleSessionPersistence, - persistSession, - authUrl: externalAuthUrl, - } = options; - const { googleClientId, scopes = GOOGLE_OAUTH_DEFAULT_SCOPES } = config; - - if (!chrome.identity) { - throw new OneKeyLocalError( - 'chrome.identity API is not available. ' + - 'Make sure you are running in a Chrome Extension context (not content script) ' + - 'and the "identity" permission is added to manifest.json. ' + - 'Try rebuilding the extension and reloading it in chrome://extensions.', - ); - } - - type IExtensionOAuthFlowParams = { redirectUrl: string }; - type IExtensionOAuthFlowSignInParams = { - rawNonce?: string; - expectedState?: string; - expectedOneKeyState?: string; - }; - type IExtensionOAuthFlowGetAuthUrlResult = { - authUrl: string; - signInParams: IExtensionOAuthFlowSignInParams; - }; - type IExtensionOAuthFlowSignInInput = { - callbackUrl: string; - signInParams: IExtensionOAuthFlowSignInParams; - }; - type IExtensionOAuthFlowBuilder = (params: IExtensionOAuthFlowParams) => { - getAuthUrl: () => Promise; - signIn: ( - input: IExtensionOAuthFlowSignInInput, - ) => Promise; - }; - - const launchWebAuthFlowWithTimeout = async (url: string) => { - let timeoutId: ReturnType | null = null; - try { - return await Promise.race([ - chrome.identity.launchWebAuthFlow({ - url, - interactive: true, - }), - new Promise((_, reject) => { - timeoutId = setTimeout(() => { - reject(new OneKeyLocalError('OAuth sign-in timed out')); - }, OAUTH_FLOW_TIMEOUT_MS); - }), - ]); - } finally { - if (timeoutId) { - clearTimeout(timeoutId); - timeoutId = null; - } - } - }; - - const buildPkceFlowParams = ({ redirectUrl }: IExtensionOAuthFlowParams) => ({ - getAuthUrl: async () => { - const authUrl = externalAuthUrl; - if (!authUrl) { - throw new OneKeyLocalError('Failed to create Supabase OAuth URL.'); - } - let expectedState: string | undefined; - let expectedOneKeyState: string | undefined; - try { - const authUrlObj = new URL(authUrl); - expectedState = authUrlObj.searchParams.get('state') ?? undefined; - - // Parse our own state from the embedded redirect_to URL (defense-in-depth) - const redirectTo = authUrlObj.searchParams.get('redirect_to'); - if (redirectTo) { - try { - const redirectToUrl = new URL(redirectTo); - expectedOneKeyState = - redirectToUrl.searchParams.get(ONEKEY_OAUTH_STATE_KEY) ?? - undefined; - } catch { - expectedOneKeyState = undefined; - } - } - } catch { - expectedState = undefined; - expectedOneKeyState = undefined; - } - return { - authUrl, - signInParams: { expectedState, expectedOneKeyState }, - }; - }, - signIn: async ({ - callbackUrl, - signInParams, - }: IExtensionOAuthFlowSignInInput) => { - // https://.chromiumapp.org/?code=xxxx - const url = new URL(callbackUrl); - if (!callbackUrl.startsWith(redirectUrl)) { - throw new OneKeyLocalError('Invalid OAuth redirect URL'); - } - const error = - url.searchParams.get('error') || - url.searchParams.get('error_description'); - if (error) { - throw new OneKeyLocalError(error); - } - - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state'); - const oneKeyState = url.searchParams.get(ONEKEY_OAUTH_STATE_KEY); - - if (!code) { - throw new OneKeyLocalError('Authorization code is missing'); - } - - // Validate OneKey state (defense-in-depth). - if (!oneKeyState) { - throw new OneKeyLocalError('OAuth state is missing'); - } - - if (!signInParams.expectedOneKeyState) { - throw new OneKeyLocalError('Expected OneKey OAuth state is missing'); - } - - if (oneKeyState !== signInParams.expectedOneKeyState) { - throw new OneKeyLocalError('OAuth state mismatch'); - } - - // Validate state (anti-CSRF / anti-injection). Supabase OAuth URLs should include `state=...` - // and the redirect callback should echo it back. - if (signInParams.expectedState) { - if (!state) { - throw new OneKeyLocalError('OAuth state is missing'); - } - if (state !== signInParams.expectedState) { - throw new OneKeyLocalError('OAuth state mismatch'); - } - } - - const { data, error: exchangeError } = - await client.auth.exchangeCodeForSession(code); - if (exchangeError) { - throw new OneKeyLocalError(exchangeError.message); - } - - if (!data.session) { - throw new OneKeyLocalError( - 'Failed to exchange authorization code for session', - ); - } - - const accessToken = data.session.access_token; - const refreshToken = data.session.refresh_token; - - await handleSessionPersistence({ - accessToken, - refreshToken, - persistSession, - }); - - return { - success: true, - session: { - accessToken, - refreshToken, - }, - }; - }, - }); - - const buildOidcFlowParams = ({ redirectUrl }: IExtensionOAuthFlowParams) => ({ - getAuthUrl: async () => { - // Build Google OAuth URL manually with response_type=id_token - // This is the key difference from the standard OAuth flow - const authUrl = new URL(GOOGLE_OAUTH_AUTHORIZE_URL); - - authUrl.searchParams.set('client_id', googleClientId); - authUrl.searchParams.set('response_type', 'id_token'); - authUrl.searchParams.set('access_type', 'offline'); - authUrl.searchParams.set('redirect_uri', redirectUrl); - authUrl.searchParams.set('scope', scopes.join(' ')); - // Generate a random nonce for security - // Supabase requires: hash the nonce before sending to Google, but pass raw nonce to Supabase - const rawNonce = crypto.randomUUID(); - // Hash the nonce using SHA-256 for Google OAuth - const encoder = new TextEncoder(); - const nonceData = encoder.encode(rawNonce); - const hashBuffer = await crypto.subtle.digest('SHA-256', nonceData); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashedNonce = hashArray - .map((b) => b.toString(16).padStart(2, '0')) - .join(''); - // Pass hashed nonce to Google OAuth URL - authUrl.searchParams.set('nonce', hashedNonce); - // Force account selection - authUrl.searchParams.set('prompt', 'select_account'); - - return { - authUrl: authUrl.href, - signInParams: { rawNonce }, - }; - }, - signIn: async ({ - callbackUrl, - signInParams, - }: IExtensionOAuthFlowSignInInput) => { - const rawNonce = signInParams.rawNonce; - if (!rawNonce) { - throw new OneKeyLocalError('Missing nonce for Google OAuth sign-in.'); - } - // Parse id_token from the callback URL hash - const url = new URL(callbackUrl); - const hashParams = new URLSearchParams(url.hash.substring(1)); - const idToken = hashParams.get(OAUTH_TOKEN_KEY_ID_TOKEN); - - if (!idToken) { - throw new OneKeyLocalError('No ID token received from Google OAuth'); - } - - // Exchange ID token for Supabase session using signInWithIdToken - // Pass raw nonce to Supabase (not hashed) - // Use a temporary client that doesn't persist sessions automatically - // This allows us to get the session data without persisting it automatically - const tempClient = client; - const { data, error } = await tempClient.auth.signInWithIdToken({ - provider: 'google', - token: idToken, - nonce: rawNonce, // Pass raw nonce to Supabase - }); - - if (error) { - throw new OneKeyLocalError(error.message); - } - - if (!data.session) { - throw new OneKeyLocalError('Failed to exchange ID token for session'); - } - - const accessToken = data.session.access_token; - const refreshToken = data.session.refresh_token; - - // Handle session persistence (only if persistSession is true) - await handleSessionPersistence({ - accessToken, - refreshToken, - persistSession, - }); - - return { - success: true, - session: { - accessToken, - refreshToken, - }, - }; - }, - }); - - const processFlow = async () => { - const redirectUrl = getRedirectUrl(); - - const { getAuthUrl, signIn } = EXTENSION_OAUTH_USE_PKCE_FLOW - ? buildPkceFlowParams({ redirectUrl }) - : buildOidcFlowParams({ redirectUrl }); - - const result = await getAuthUrl(); - const authUrl = result.authUrl; - const signInParams = result.signInParams; - - const callbackUrl = await launchWebAuthFlowWithTimeout(authUrl); - - if (!callbackUrl) { - throw new OneKeyLocalError( - 'OAuth authentication failed: callback URL is missing', - ); - } - - return signIn({ callbackUrl, signInParams }); - }; - - // Launch the OAuth flow - // Note: chrome.identity.launchWebAuthFlow doesn't support window size/position options - // Chrome controls the OAuth window and may not allow modifications - // We'll set up a listener to try updating the window when it's created - const width = OAUTH_POPUP_WIDTH; - const height = OAUTH_POPUP_HEIGHT; - const left = Math.round((globalThis.screen?.width || 1920) / 2 - width / 2); - const top = Math.round((globalThis.screen?.height || 1080) / 2 - height / 2); - - // Set up a one-time listener to catch the OAuth window when it's created - // Declare outside try block so it can be cleaned up in catch - let windowUpdateListener: ((window: chrome.windows.Window) => void) | null = - null; - let focusInterval: ReturnType | null = null; - let oauthWindowId: number | null = null; - - windowUpdateListener = (window: chrome.windows.Window) => { - if (window.type === 'popup' && window.id) { - oauthWindowId = window.id; - // Try to update window size and position - // Note: Chrome may ignore these updates for OAuth windows - chrome.windows - .update(window.id, { - width, - height, - left, - top, - focused: true, // Try to bring window to front - }) - .catch(() => { - // Ignore errors - Chrome may not allow updating OAuth windows - }); - - // Set up a polling interval to periodically focus the OAuth window - // Similar to web OAuth popup behavior (openOAuthPopupWeb.tsx) - // This ensures the OAuth window stays in front during the authentication flow - focusInterval = setInterval(() => { - if (oauthWindowId !== null) { - chrome.windows.update(oauthWindowId, { focused: true }).catch(() => { - // Ignore errors - window may be closed or Chrome may not allow focusing - }); - } - }, OAUTH_POLL_INTERVAL_MS); // Same cadence as web implementation - - // Remove listener after first window is found - if (windowUpdateListener) { - chrome.windows.onCreated.removeListener(windowUpdateListener); - } - } - }; - - chrome.windows.onCreated.addListener(windowUpdateListener); - - const cleanup = () => { - if (windowUpdateListener) { - chrome.windows.onCreated.removeListener(windowUpdateListener); - } - if (focusInterval !== null) { - clearInterval(focusInterval); - focusInterval = null; - } - }; - - try { - // -------------------------------------------------------------------------- - // PKCE mode (Supabase OAuth URL + exchangeCodeForSession) - // - // Still uses: - // - chrome.identity.launchWebAuthFlow() - // so it matches the extension constraint and avoids relying on window.open(). - // - // When true, use Supabase OAuth + PKCE code flow and exchange the returned code for a session. - // When false (default), use Google OIDC id_token + nonce and exchange via signInWithIdToken(). - // -------------------------------------------------------------------------- - return await processFlow(); - } catch (error) { - // User closed the popup or other error - if ( - error instanceof Error && - error.message.includes('The user did not approve') - ) { - throw new OneKeyLocalError('OAuth sign-in was cancelled'); - } - throw new OneKeyLocalError( - error instanceof Error ? error.message : 'Extension OAuth failed', - ); - } finally { - try { - cleanup(); - } catch { - // Ignore cleanup errors to avoid masking the original OAuth error. - } - } -} - -/** - * OAuth helper for Chrome Extension using getChromeApi().identity.getAuthToken - * - * This is an ALTERNATIVE method that uses Chrome's built-in OAuth via manifest oauth2 config. - * - * How it works: - * 1. Uses getChromeApi().identity.getAuthToken to get Google Access Token (via manifest oauth2 config) - * 2. Fetches user info from Google to get the ID Token - * 3. Uses Supabase signInWithIdToken to exchange for session - * - * Prerequisites: - * - manifest.json must have oauth2.client_id and oauth2.scopes configured - * - Google Cloud Console: Create Chrome Extension type OAuth Client ID - * - Supabase Dashboard: Add Chrome Extension Client ID to Google Provider - * - * manifest.json example: - * { - * "oauth2": { - * "client_id": "YOUR_CHROME_CLIENT_ID.apps.googleusercontent.com", - * "scopes": ["openid", "email", "profile"] - * } - * } - * - * @param options - Configuration options - * @param options.handleSessionPersistence - Function to handle session persistence - * @param options.persistSession - Whether to persist the session - * @returns Promise with success status and session tokens - */ -export async function openOAuthPopupExtIdToken(_options: { - handleSessionPersistence: ( - params: IHandleOAuthSessionPersistenceParams, - ) => Promise; - persistSession?: boolean; -}): Promise { - if (!chrome.identity) { - throw new OneKeyLocalError( - 'chrome.identity API is not available. ' + - 'Make sure you are running in a Chrome Extension context (not content script) ' + - 'and the "identity" permission is added to manifest.json. ' + - 'Try rebuilding the extension and reloading it in chrome://extensions.', - ); - } - - try { - // Step 1: Get Google Access Token using chrome.identity.getAuthToken - // This requires oauth2 config in manifest.json - const authResult = await chrome.identity.getAuthToken({ - interactive: true, - }); - - if (!authResult.token) { - throw new OneKeyLocalError('Failed to get Google auth token'); - } - - const googleAccessToken = authResult.token; - - // Step 2: Fetch user info from Google to get the ID token - // We need to use the tokeninfo endpoint to get additional token details - const tokenInfoResponse = await fetch( - `${GOOGLE_OAUTH_TOKENINFO_URL}?${OAUTH_TOKEN_KEY_ACCESS_TOKEN}=${googleAccessToken}`, - ); - - if (!tokenInfoResponse.ok) { - throw new OneKeyLocalError('Failed to validate Google token'); - } - - // Step 3: Get the ID token by making an OAuth request - // Since getAuthToken doesn't directly return id_token, we need to use a different approach - // We'll use the access token to get user info and then exchange with Supabase - const userInfoResponse = await fetch(GOOGLE_OAUTH_USERINFO_URL, { - headers: { - Authorization: `Bearer ${googleAccessToken}`, - }, - }); - - if (!userInfoResponse.ok) { - throw new OneKeyLocalError('Failed to get user info from Google'); - } - - const userInfo = (await userInfoResponse.json()) as { - sub: string; - email: string; - email_verified: boolean; - name: string; - picture: string; - }; - - // Note: getAuthToken doesn't provide id_token directly - // We need to use launchWebAuthFlow with response_type=id_token for proper ID token - // This method is kept as an alternative but openOAuthPopupExtIdentity is preferred - - // For now, we'll throw an error indicating this method needs the manifest oauth2 config - // with proper setup to get id_token - throw new OneKeyLocalError( - 'getAuthToken method requires id_token. Please use CHROME_IDENTITY_API method instead, ' + - `or configure manifest oauth2. User email: ${userInfo.email}`, - ); - - // Uncomment below if you have a way to get id_token from getAuthToken flow: - // const { data, error } = await supabaseClient.auth.signInWithIdToken({ - // provider: 'google', - // token: idToken, // Need to get this from somewhere - // access_token: googleAccessToken, - // }); - } catch (error) { - if ( - error instanceof Error && - error.message.includes('The user did not approve') - ) { - throw new OneKeyLocalError('OAuth sign-in was cancelled'); - } - throw new OneKeyLocalError( - error instanceof Error ? error.message : 'Extension OAuth failed', - ); - } -} - -/** - * OAuth popup window helper for Chrome Extension platform - * - * ⚠️ WARNING: THIS METHOD DOES NOT WORK ⚠️ - * - * This method attempts to use the direct chrome-extension:// scheme for OAuth callback, - * but it FAILS because Chrome blocks external websites from redirecting to chrome-extension:// URLs. - * - * Error: ERR_BLOCKED_BY_CLIENT - * Reason: Chrome security restriction prevents web pages from redirecting to extension URLs - * to protect users from malicious websites triggering extension actions. - * - * OAuth flow that fails: - * Google/Apple OAuth → Supabase → chrome-extension://xxx/ui-oauth-callback.html - * ↑ Chrome blocks this redirect - * - * RECOMMENDED: Use CHROME_IDENTITY_API method instead (openOAuthPopupExtIdentity) - * - Uses getChromeApi().identity.launchWebAuthFlow API - * - Redirect URL: https://.chromiumapp.org/auth/callback - * - Chrome specially handles .chromiumapp.org URLs for extension OAuth - * - * This code is kept for reference but should NOT be used. - * - * --- - * Original design (non-functional): - * Uses getChromeApi().windows.create to open a popup window for OAuth authentication. - * Monitors tab URL changes to detect the OAuth callback and extract tokens. - * - * This method uses the direct chrome-extension:// scheme: - * - Opens a popup window for OAuth - * - Monitors URL changes via getChromeApi().tabs.onUpdated - * - Extracts tokens when URL matches chrome-extension:///ui-oauth-callback.html - * - * Supabase Redirect URL to add: - * chrome-extension:///ui-oauth-callback.html - * - * @param options - Configuration options - * @param options.authUrl - The OAuth authorization URL to open - * @param options.handleSessionPersistence - Function to handle session persistence - * @param options.persistSession - Whether to persist the session - * @returns Promise with success status and session tokens - * @deprecated Use openOAuthPopupExtIdentity instead - this method does not work due to Chrome security restrictions - */ -export function openOAuthPopupExtWindow(options: { - authUrl: string; - handleSessionPersistence: ( - params: IHandleOAuthSessionPersistenceParams, - ) => Promise; - persistSession?: boolean; -}): Promise { - const { authUrl, handleSessionPersistence, persistSession } = options; - - return new Promise((resolve, reject) => { - // Popup window dimensions (same as web OAuth popup) - const width = OAUTH_POPUP_WIDTH; - const height = OAUTH_POPUP_HEIGHT; - let windowId: number | undefined; - let resolved = false; - - // Helper to close window safely - const closeWindow = () => { - if (windowId !== undefined) { - try { - void chrome.windows.remove(windowId); - } catch { - // Window may already be closed - } - } - }; - - // Store listener references for cleanup - const listeners = { - onTabUpdated: null as - | (( - tabId: number, - changeInfo: chrome.tabs.TabChangeInfo, - tab: chrome.tabs.Tab, - ) => void) - | null, - onWindowRemoved: null as ((removedWindowId: number) => void) | null, - }; - - // Helper to remove all listeners - const cleanup = () => { - if (listeners.onTabUpdated) { - chrome.tabs.onUpdated.removeListener(listeners.onTabUpdated); - } - if (listeners.onWindowRemoved) { - chrome.windows.onRemoved.removeListener(listeners.onWindowRemoved); - } - }; - - // Listen for window close (user cancelled) - listeners.onWindowRemoved = (removedWindowId: number) => { - if (removedWindowId === windowId && !resolved) { - resolved = true; - cleanup(); - reject(new OneKeyLocalError('OAuth sign-in was cancelled')); - } - }; - - // Listen for tab URL changes to detect OAuth callback - listeners.onTabUpdated = ( - _tabId: number, - changeInfo: chrome.tabs.TabChangeInfo, - tab: chrome.tabs.Tab, - ) => { - // Only process tabs in our OAuth window - if (tab.windowId !== windowId || resolved) { - return; - } - - // Check multiple URL sources: - // - changeInfo.url: When URL changes during navigation - // - tab.url: Current tab URL - // - tab.pendingUrl: URL that the tab is navigating to (useful when navigation is blocked) - const tabUrl = - changeInfo.url || - tab.url || - (tab as chrome.tabs.Tab & { pendingUrl?: string }).pendingUrl; - if (!tabUrl) { - return; - } - - // Check if URL is our callback URL with tokens - if ( - tabUrl.startsWith( - `chrome-extension://${chrome.runtime.id}/ui-oauth-callback.html`, - ) - ) { - resolved = true; - cleanup(); - closeWindow(); - - // Parse tokens from the URL - try { - const parsedUrl = new URL(tabUrl); - const hashParams = new URLSearchParams( - parsedUrl.hash.substring(1) || parsedUrl.search.substring(1), - ); - - const accessToken = hashParams.get(OAUTH_TOKEN_KEY_ACCESS_TOKEN); - const refreshToken = hashParams.get(OAUTH_TOKEN_KEY_REFRESH_TOKEN); - - if (accessToken && refreshToken) { - void handleSessionPersistence({ - accessToken, - refreshToken, - persistSession, - }).then(() => { - resolve({ - success: true, - session: { - accessToken, - refreshToken, - }, - }); - }); - } else { - reject( - new OneKeyLocalError( - 'OAuth authentication failed: access token or refresh token is missing', - ), - ); - } - } catch (error) { - reject( - new OneKeyLocalError( - error instanceof Error - ? error.message - : 'Failed to process OAuth callback', - ), - ); - } - } - }; - - // Add listeners before creating window - chrome.tabs.onUpdated.addListener(listeners.onTabUpdated); - chrome.windows.onRemoved.addListener(listeners.onWindowRemoved); - - // Create popup window for OAuth - chrome.windows.create( - { - url: authUrl, - type: 'popup', - width, - height, - // Center the window on screen - left: Math.round((globalThis.screen?.width || 1920) / 2 - width / 2), - top: Math.round((globalThis.screen?.height || 1080) / 2 - height / 2), - }, - (authWindow) => { - if (!authWindow?.id) { - cleanup(); - reject(new OneKeyLocalError('Failed to create OAuth window')); - return; - } - - windowId = authWindow.id; - - // Set timeout - setTimeout(() => { - if (!resolved) { - resolved = true; - cleanup(); - closeWindow(); - reject(new OneKeyLocalError('OAuth sign-in timed out')); - } - }, OAUTH_FLOW_TIMEOUT_MS); // 5 minutes timeout - }, - ); - }); -} diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupNative.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupNative.tsx deleted file mode 100644 index 66d61612f5f1..000000000000 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupNative.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import * as WebBrowser from 'expo-web-browser'; - -import { - OAUTH_TOKEN_KEY_ACCESS_TOKEN, - OAUTH_TOKEN_KEY_REFRESH_TOKEN, -} from '@onekeyhq/shared/src/consts/authConsts'; -import { ONEKEY_APP_DEEP_LINK } from '@onekeyhq/shared/src/consts/deeplinkConsts'; -import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; - -import type { - IHandleOAuthSessionPersistenceParams, - IOAuthPopupResult, -} from './openOAuthPopupTypes'; - -/** - * Get OAuth redirect URL for native platforms (iOS/Android) - * - * Uses the deep link scheme: onekey-wallet://auth/callback - * - * @returns The redirect URL for native OAuth - */ -export function getOAuthRedirectUrlNative(): string { - return `${ONEKEY_APP_DEEP_LINK}auth/callback`; -} - -/** - * OAuth helper for native platforms (iOS/Android) - * - * Uses expo-web-browser to open an in-app browser for OAuth authentication. - * The browser will redirect to the deep link callback URL when complete. - * - * @param options - Configuration options - * @param options.authUrl - The OAuth authorization URL to open - * @param options.redirectTo - The redirect URL for OAuth callback - * @param options.handleSessionPersistence - Function to handle session persistence - * @param options.persistSession - Whether to persist the session - * @returns Promise with success status and session tokens - */ -export async function openOAuthPopupNative(options: { - authUrl: string; - redirectTo: string | undefined; - handleSessionPersistence: ( - params: IHandleOAuthSessionPersistenceParams, - ) => Promise; - persistSession?: boolean; -}): Promise { - const { authUrl, redirectTo, handleSessionPersistence, persistSession } = - options; - - // Use expo-web-browser for native platforms - // eslint-disable-next-line spellcheck/spell-checker - const browserResult = await WebBrowser.openAuthSessionAsync( - authUrl, - redirectTo, - { - // eslint-disable-next-line spellcheck/spell-checker - showInRecents: true, - preferEphemeralSession: false, - }, - ); - - if (browserResult.type === 'success' && browserResult.url) { - // Extract tokens from the callback URL - const url = new URL(browserResult.url); - const hashParams = new URLSearchParams( - url.hash.substring(1) || url.search.substring(1), - ); - - const accessToken = hashParams.get(OAUTH_TOKEN_KEY_ACCESS_TOKEN); - const refreshToken = hashParams.get(OAUTH_TOKEN_KEY_REFRESH_TOKEN); - - if (accessToken && refreshToken) { - await handleSessionPersistence({ - accessToken, - refreshToken, - persistSession, - }); - - return { - success: true, - session: { - accessToken, - refreshToken, - }, - }; - } - } - - if (browserResult.type === 'cancel') { - throw new OneKeyLocalError('OAuth sign-in was cancelled'); - } - - throw new OneKeyLocalError('OAuth sign-in failed'); -} diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupTypes.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupTypes.tsx deleted file mode 100644 index 2fca076bc4d4..000000000000 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupTypes.tsx +++ /dev/null @@ -1,31 +0,0 @@ -export type IHandleOAuthSessionPersistenceParams = { - accessToken: string; - refreshToken: string; -}; - -export type IOAuthPopupResult = { - success: boolean; - session?: { - accessToken: string; - refreshToken: string; - }; -}; - -export type IOpenOAuthPopupOptions = { - // Whether to persist the session to storage - // When false (default): Only return tokens, don't call setSession - persistSession?: boolean; -}; - -/** - * OAuth configuration for Google sign-in (extension). - * These values should match your Google Cloud Console OAuth 2.0 Client ID settings. - */ -export interface IExtensionOAuthConfig { - // Google OAuth Client ID for Chrome Extension - // Create this in Google Cloud Console > APIs & Services > Credentials > OAuth 2.0 Client IDs - // Application type: Chrome Extension - googleClientId: string; - // OAuth scopes to request - scopes?: string[]; -} diff --git a/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx b/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx deleted file mode 100644 index 3aeebcff2bdf..000000000000 --- a/packages/kit/src/components/OneKeyAuth/openOAuthPopupWeb.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import { - OAUTH_CALLBACK_WEB_PATH, - OAUTH_FLOW_TIMEOUT_MS, - OAUTH_POLL_INTERVAL_MS, - OAUTH_POPUP_HEIGHT, - OAUTH_POPUP_WIDTH, -} from '@onekeyhq/shared/src/consts/authConsts'; -import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; - -import type { - IHandleOAuthSessionPersistenceParams, - IOAuthPopupResult, -} from './openOAuthPopupTypes'; -import type { SupabaseClient } from '@supabase/supabase-js'; - -const ONEKEY_OAUTH_STATE_KEY = 'onekey_oauth_state'; - -/** - * Get OAuth redirect URL for web platform - * - * Uses the current origin with /oauth_callback_web path - * Example: https://app.onekey.so/oauth_callback_web - * - * @returns The redirect URL for web OAuth - */ -export function getOAuthRedirectUrlWeb(): string { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `${globalThis.location?.origin || ''}${OAUTH_CALLBACK_WEB_PATH}`; -} - -// Focus the popup window to bring it to front, with error handling -function focusPopup(win: Window | null) { - try { - win?.focus(); - } catch (e) { - // Focusing may fail (e.g., popup not allowed or browser restrictions) - // We silently ignore the error as it does not affect auth flow - } -} - -// Close the popup window safely, with error handling -function closePopup(win: Window | null) { - try { - win?.close(); - } catch (e) { - // Closing may fail (e.g., popup already closed or browser restrictions) - // We silently ignore the error as the popup is either already closed or inaccessible - } -} - -/** - * OAuth popup window helper for web platform - * - * Opens a popup window for OAuth authentication and monitors for the callback URL. - * Extracts tokens from the URL when authentication is complete. - * - * @param options - Configuration options - * @param options.authUrl - The OAuth authorization URL to open - * @param options.client - Supabase client instance - * @param options.handleSessionPersistence - Function to handle session persistence - * @param options.persistSession - Whether to persist the session (default: false) - * @returns Promise with success status and session tokens - */ -export async function openOAuthPopupWeb(options: { - authUrl: string; - client: SupabaseClient; - handleSessionPersistence: ( - params: IHandleOAuthSessionPersistenceParams, - ) => Promise; - persistSession?: boolean; -}): Promise { - const { authUrl, client, handleSessionPersistence, persistSession } = options; - - return new Promise((resolve, reject) => { - let settled = false; - let inFlight = false; - let pollIntervalId: ReturnType | null = null; - let timeoutId: ReturnType | null = null; - let expectedState: string | null = null; - let expectedOneKeyState: string | null = null; - - try { - // "https://xxx.supabase.co/auth/v1/authorize?provider=google&redirect_to=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback&code_challenge=xxxx-xxx&code_challenge_method=s256&prompt=select_account" - const authUrlObj = new URL(authUrl); - expectedState = authUrlObj.searchParams.get('state'); - - // Parse our own state from the embedded redirect_to URL (defense-in-depth) - const redirectTo = authUrlObj.searchParams.get('redirect_to'); - if (redirectTo) { - try { - const redirectUrl = new URL(redirectTo); - expectedOneKeyState = redirectUrl.searchParams.get( - ONEKEY_OAUTH_STATE_KEY, - ); - } catch { - expectedOneKeyState = null; - } - } - } catch { - expectedState = null; - expectedOneKeyState = null; - } - - const cleanup = (popup: Window | null) => { - if (pollIntervalId) { - clearInterval(pollIntervalId); - pollIntervalId = null; - } - if (timeoutId) { - clearTimeout(timeoutId); - timeoutId = null; - } - if (popup && !popup.closed) { - closePopup(popup); - } - }; - - const resolveOnce = (result: IOAuthPopupResult, popup: Window | null) => { - if (settled) { - return; - } - settled = true; - cleanup(popup); - resolve(result); - }; - - const rejectOnce = (error: unknown, popup: Window | null) => { - if (settled) { - return; - } - settled = true; - cleanup(popup); - reject(error); - }; - - // Calculate popup window position (centered) - const width = OAUTH_POPUP_WIDTH; - const height = OAUTH_POPUP_HEIGHT; - const left = globalThis.screenX + (globalThis.outerWidth - width) / 2; - const top = globalThis.screenY + (globalThis.outerHeight - height) / 2; - - // Open popup window without address bar and toolbar - // Note: Web browsers don't allow forcing popups to stay on top (alwaysOnTop) - // for security reasons. We can only focus the popup when it opens. - const popup = globalThis.open( - authUrl, - 'oauth_popup', - `width=${width},height=${height},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,scrollbars=yes,resizable=yes`, - ); - - if (!popup) { - rejectOnce( - new OneKeyLocalError( - 'Popup was blocked. Please allow popups and try again.', - ), - null, - ); - return; - } - - focusPopup(popup); - - // Poll for popup close and check for auth code (PKCE flow) - pollIntervalId = setInterval(async () => { - if (inFlight) { - return; - } - inFlight = true; - try { - if (settled) { - return; - } - focusPopup(popup); - // Check if popup is closed - if (popup.closed) { - // Check if we got a session after popup closed - const { data } = await client.auth.getSession(); - if (data.session) { - const accessToken = data.session.access_token; - const refreshToken = data.session.refresh_token; - - await handleSessionPersistence({ - accessToken, - refreshToken, - }); - - resolveOnce( - { - success: true, - session: { - accessToken, - refreshToken, - }, - }, - popup, - ); - } else { - rejectOnce( - new OneKeyLocalError( - 'OAuth authentication failed: no session found after popup closed', - ), - popup, - ); - } - return; - } - - // Try to read the popup URL to check for callback with authorization code - try { - const popupUrl = popup.location.href; - // PKCE flow: check for 'code' parameter in URL (not access_token) - if (popupUrl && popupUrl.includes('code=')) { - closePopup(popup); - - // Parse authorization code from URL query string - const url = new URL(popupUrl); - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state'); - const oneKeyState = url.searchParams.get(ONEKEY_OAUTH_STATE_KEY); - - if (!code) { - rejectOnce( - new OneKeyLocalError('Authorization code is missing'), - popup, - ); - return; - } - - if (!expectedOneKeyState) { - rejectOnce( - new OneKeyLocalError('Expected OneKey OAuth state is missing'), - popup, - ); - return; - } - - // Validate OneKey state (defense-in-depth). - if (!oneKeyState) { - rejectOnce(new OneKeyLocalError('OAuth state is missing'), popup); - return; - } - - if (oneKeyState !== expectedOneKeyState) { - rejectOnce(new OneKeyLocalError('OAuth state mismatch'), popup); - return; - } - - // Validate state (anti-CSRF / anti-injection). Supabase OAuth URLs should include `state=...` - // and the redirect callback should echo it back. - if (expectedState) { - if (!state) { - rejectOnce( - new OneKeyLocalError('OAuth state is missing'), - popup, - ); - return; - } - if (state !== expectedState) { - rejectOnce(new OneKeyLocalError('OAuth state mismatch'), popup); - return; - } - } - - // Exchange authorization code for session tokens using PKCE - // The Supabase client automatically uses the stored code_verifier - const { data, error } = await client.auth.exchangeCodeForSession( - code, - ); - - if (error) { - rejectOnce(new OneKeyLocalError(error.message), popup); - return; - } - - const session = data.session; - if (!session) { - rejectOnce( - new OneKeyLocalError( - 'Failed to exchange authorization code for session', - ), - popup, - ); - return; - } - - const accessToken = session.access_token; - const refreshToken = session.refresh_token; - - await handleSessionPersistence({ - accessToken, - refreshToken, - }); - - resolveOnce( - { - success: true, - session: { - accessToken, - refreshToken, - }, - }, - popup, - ); - } - } catch { - // Cross-origin error - popup is on different domain, continue polling - } - } catch (error) { - rejectOnce(error, popup); - } finally { - inFlight = false; - } - }, OAUTH_POLL_INTERVAL_MS); - - // Cleanup after timeout (5 minutes) - timeoutId = setTimeout(() => { - rejectOnce(new OneKeyLocalError('OAuth sign-in timed out'), popup); - }, OAUTH_FLOW_TIMEOUT_MS); - }); -} From 6ebc6f77c65403f22e8169d6237c660736ec887d Mon Sep 17 00:00:00 2001 From: morizon Date: Mon, 22 Dec 2025 17:20:54 +0800 Subject: [PATCH 11/66] fix: google sign in for iOS --- .../OneKeyWallet.xcodeproj/project.pbxproj | 4 + apps/mobile/ios/Podfile.lock | 30 +++ apps/mobile/package.json | 4 +- .../OAuthPopup/GOOGLE_SIGNIN_SETUP.md | 235 ++++++++++++++++++ .../OAuthPopup/OAuthPopup.desktop.tsx | 2 +- .../OAuthPopup/OAuthPopup.native.tsx | 59 +++-- .../components/OneKeyAuth/OAuthPopup/types.ts | 18 -- packages/shared/src/consts/authConsts.ts | 9 +- .../shared/src/consts/googleSignConsts.ts | 17 +- 9 files changed, 329 insertions(+), 49 deletions(-) create mode 100644 packages/kit/src/components/OneKeyAuth/OAuthPopup/GOOGLE_SIGNIN_SETUP.md diff --git a/apps/mobile/ios/OneKeyWallet.xcodeproj/project.pbxproj b/apps/mobile/ios/OneKeyWallet.xcodeproj/project.pbxproj index e643666eb868..7f454d5d5e28 100644 --- a/apps/mobile/ios/OneKeyWallet.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/OneKeyWallet.xcodeproj/project.pbxproj @@ -483,6 +483,7 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-OneKeyWallet/Pods-OneKeyWallet-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/AppAuth/AppAuthCore_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/CocoaLumberjack/CocoaLumberjackPrivacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", @@ -491,6 +492,7 @@ "${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle", @@ -514,6 +516,7 @@ ); name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AppAuthCore_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/CocoaLumberjackPrivacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", @@ -522,6 +525,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle", diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock index f03e06899b59..c98022e66966 100644 --- a/apps/mobile/ios/Podfile.lock +++ b/apps/mobile/ios/Podfile.lock @@ -1,4 +1,10 @@ PODS: + - AppAuth (1.7.6): + - AppAuth/Core (= 1.7.6) + - AppAuth/ExternalUserAgent (= 1.7.6) + - AppAuth/Core (1.7.6) + - AppAuth/ExternalUserAgent (1.7.6): + - AppAuth/Core - BackgroundThread (1.1.11): - boost - DoubleConversion @@ -136,6 +142,10 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga + - ExpoAdapterGoogleSignIn (9.1.0): + - ExpoModulesCore + - GoogleSignIn (~> 6.2) + - React-Core - ExpoAppleAuthentication (7.2.4): - ExpoModulesCore - ExpoAsset (12.0.10): @@ -251,6 +261,14 @@ PODS: - FBLazyVector (0.81.5) - fmt (11.0.2) - glog (0.3.5) + - GoogleSignIn (6.2.4): + - AppAuth (~> 1.5) + - GTMAppAuth (~> 1.3) + - GTMSessionFetcher/Core (< 3.0, >= 1.1) + - GTMAppAuth (1.3.1): + - AppAuth/Core (~> 1.6) + - GTMSessionFetcher/Core (< 3.0, >= 1.5) + - GTMSessionFetcher/Core (2.3.0) - hermes-engine (0.81.5): - hermes-engine/Pre-built (= 0.81.5) - hermes-engine/Pre-built (0.81.5) @@ -3933,6 +3951,7 @@ DEPENDENCIES: - EXImageLoader (from `../../../node_modules/expo-image-loader/ios`) - EXNotifications (from `../../../node_modules/expo-notifications/ios`) - Expo (from `../../../node_modules/expo`) + - "ExpoAdapterGoogleSignIn (from `../../../node_modules/@react-native-google-signin/google-signin/expo/ios`)" - ExpoAppleAuthentication (from `../../../node_modules/expo-apple-authentication/ios`) - ExpoAsset (from `../../../node_modules/expo-asset/ios`) - ExpoBlur (from `../../../node_modules/expo-blur/ios`) @@ -4084,8 +4103,12 @@ SPEC REPOS: https://github.com/aliyun/aliyun-specs.git: - EMASCurl https://github.com/CocoaPods/Specs.git: + - AppAuth - CocoaAsyncSocket - CocoaLumberjack + - GoogleSignIn + - GTMAppAuth + - GTMSessionFetcher - libwebp - lottie-ios - MMKVCore @@ -4127,6 +4150,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/expo-notifications/ios" Expo: :path: "../../../node_modules/expo" + ExpoAdapterGoogleSignIn: + :path: "../../../node_modules/@react-native-google-signin/google-signin/expo/ios" ExpoAppleAuthentication: :path: "../../../node_modules/expo-apple-authentication/ios" ExpoAsset: @@ -4418,6 +4443,7 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: + AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 BackgroundThread: d6bedf61338886caa616744dc1e4aa77656dc285 BleUtils: 0fd18e5fd2732c89771dc79941c7320ac9c42015 boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 @@ -4432,6 +4458,7 @@ SPEC CHECKSUMS: EXImageLoader: 4d3d3284141f1a45006cc4d0844061c182daf7ee EXNotifications: 1afe5275cf399920480fc5a9008676749a767d19 Expo: 22c06b7185706d92688f6e943576198540836bc1 + ExpoAdapterGoogleSignIn: ddfb635edae1ea92679a18ea34cb6ef6dbd69f03 ExpoAppleAuthentication: 4d2e0c88a4463229760f1fbb9a937a810efb6863 ExpoAsset: d839c8eae8124470332408427327e8f88beb2dfd ExpoBlur: 2dd8f64aa31f5d405652c21d3deb2d2588b1852f @@ -4461,6 +4488,9 @@ SPEC CHECKSUMS: FBLazyVector: 5beb8028d5a2e75dd9634917f23e23d3a061d2aa fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 + GoogleSignIn: 5651ce3a61e56ca864160e79b484cd9ed3f49b7a + GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd + GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172 ImageColors: 51cd79f7a9d2524b7a681c660b0a50574085563b JCoreRN: d985de509185b381c177fde4bb6bfc686089fdbd diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 0feefdbb7a55..0614c9f15fa2 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -160,9 +160,7 @@ "exclude": [] }, "ios": { - "exclude": [ - "@react-native-google-signin/google-signin" - ] + "exclude": [] } } } diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/GOOGLE_SIGNIN_SETUP.md b/packages/kit/src/components/OneKeyAuth/OAuthPopup/GOOGLE_SIGNIN_SETUP.md new file mode 100644 index 000000000000..effeaa098c8e --- /dev/null +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/GOOGLE_SIGNIN_SETUP.md @@ -0,0 +1,235 @@ +# Google Sign-In Setup Guide + +This document describes how to configure Google Sign-In for each platform in the OneKey app. + +## Overview + +OneKey uses Google Sign-In with Supabase for authentication. The flow is: + +1. User signs in with Google (platform-specific method) +2. Get Google ID token +3. Exchange ID token for Supabase session via `signInWithIdToken` + +## Google Cloud Console Configuration + +### Prerequisites + +1. Go to [Google Cloud Console](https://console.cloud.google.com/) +2. Select or create a project +3. Enable **Google Sign-In API** (APIs & Services → Library → Google Sign-In API) + +### OAuth Client IDs + +Create OAuth 2.0 Client IDs at: **APIs & Services → Credentials → Create Credentials → OAuth Client ID** + +| Platform | Type | Client ID | +|----------|------|-----------| +| Web | Web application | `244450898872-d22ubafv8ca38s6fp0kflhdr6e3s386u.apps.googleusercontent.com` | +| iOS | iOS | `244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt.apps.googleusercontent.com` | +| Android | Web application | Use Web Client ID (required for `idToken`) | +| Extension | Web application | Use Web Client ID | + +> **Important**: For Android/iOS native apps, you need a **Web Client ID** to get `idToken` for Supabase. The native client ID alone won't work with `signInWithIdToken`. + +--- + +## iOS Configuration + +### 1. Install Dependencies + +Ensure `@react-native-google-signin/google-signin` is NOT excluded in `apps/mobile/package.json`: + +```json +{ + "excludePackagesFromPodInstall": { + "exclude": [] // Remove @react-native-google-signin/google-signin from exclude list + } +} +``` + +### 2. Run Pod Install + +```bash +cd apps/mobile/ios +pod install +``` + +### 3. Add GoogleService-Info.plist + +1. Go to [Firebase Console](https://console.firebase.google.com/) or [Google Cloud Console](https://console.cloud.google.com/) +2. Download `GoogleService-Info.plist` for your iOS app +3. Add it to the Xcode project: + - Open `apps/mobile/ios/OneKeyWallet.xcworkspace` in Xcode + - Drag `GoogleService-Info.plist` into the project navigator + - Ensure "Copy items if needed" is checked + - Add to target: `OneKeyWallet` + +### 4. Configure URL Scheme in Info.plist + +Add the reversed client ID as a URL scheme in `apps/mobile/ios/OneKeyWallet/Info.plist`: + +```xml +CFBundleURLTypes + + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + + com.googleusercontent.apps.244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt + + + +``` + +> **Note**: The URL scheme is the iOS Client ID reversed. For `244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt.apps.googleusercontent.com`, use `com.googleusercontent.apps.244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt`. + +### 5. Verify Configuration + +The iOS Client ID in `packages/shared/src/consts/authConsts.ts` should match: + +```typescript +export const GOOGLE_OAUTH_CLIENT_IDS = { + IOS: '244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt.apps.googleusercontent.com', +}; +``` + +And in `packages/shared/src/consts/googleSignConsts.ts`: + +```typescript +export const GoogleSignInConfigureIOS = { + scopes: ['openid', 'profile', 'email'], + offlineAccess: false, + iosClientId: GOOGLE_OAUTH_CLIENT_IDS.IOS, +}; +``` + +### 6. Rebuild iOS App + +```bash +# Clean and rebuild +cd apps/mobile/ios +rm -rf build Pods +pod install +cd .. +yarn ios +``` + +--- + +## Android Configuration + +### 1. Install Dependencies + +Ensure `@react-native-google-signin/google-signin` is properly linked (automatic with autolinking). + +### 2. Add google-services.json + +1. Download `google-services.json` from Firebase Console +2. Place it in `apps/mobile/android/app/google-services.json` + +### 3. Configure Signing (Important!) + +Google Sign-In requires proper app signing. In development: + +1. Generate a debug keystore SHA-1: + ```bash + cd apps/mobile/android + ./gradlew signingReport + ``` + +2. Add the SHA-1 fingerprint to your Google Cloud OAuth client: + - Go to Google Cloud Console → APIs & Services → Credentials + - Edit your Android OAuth Client + - Add the SHA-1 fingerprint + +### 4. Verify Configuration + +The Web Client ID in `packages/shared/src/consts/googleSignConsts.ts`: + +```typescript +export const GoogleSignInConfigure = { + scopes: ['openid', 'profile', 'email'], + offlineAccess: false, + webClientId: GOOGLE_OAUTH_CLIENT_IDS.ANDROID, // Must be Web Client ID! +}; +``` + +> **Critical**: `webClientId` must be a **Web application** type OAuth client, not Android type. This is required to receive `idToken`. + +--- + +## Web Configuration + +Web platform uses Supabase OAuth flow directly (no native Google Sign-In SDK). + +### Supabase Configuration + +1. Go to [Supabase Dashboard](https://supabase.com/dashboard) → Authentication → Providers → Google +2. Enable Google provider +3. Add Web Client ID and Client Secret +4. Configure redirect URL: `https://your-domain.com/oauth_callback_web/` + +--- + +## Browser Extension Configuration + +Extension uses `chrome.identity.launchWebAuthFlow` with the Web Client ID. + +### Google Cloud Console + +1. Create/edit Web application OAuth client +2. Add authorized redirect URI: + ``` + https://.chromiumapp.org + ``` + +--- + +## Troubleshooting + +### iOS: "Cannot read property 'SIGN_IN_CANCELLED' of null" + +**Cause**: Native module not linked properly. + +**Solution**: +1. Check `@react-native-google-signin/google-signin` is not excluded +2. Run `pod install` +3. Clean build and rebuild in Xcode + +### iOS: "DEVELOPER_ERROR" or sign-in fails silently + +**Cause**: URL scheme not configured or wrong Client ID. + +**Solution**: +1. Verify URL scheme in Info.plist matches reversed iOS Client ID +2. Verify `iosClientId` in code matches Google Cloud Console + +### Android: "DEVELOPER_ERROR" + +**Cause**: SHA-1 fingerprint mismatch or wrong Client ID. + +**Solution**: +1. Run `./gradlew signingReport` to get SHA-1 +2. Add SHA-1 to Google Cloud Console OAuth client +3. Ensure `webClientId` is a Web type client (not Android) + +### All Platforms: "No ID token received" + +**Cause**: Using wrong client type or `offlineAccess` misconfigured. + +**Solution**: +1. Ensure using **Web Client ID** for `webClientId` parameter +2. For native apps, both `webClientId` (web type) and native client must be configured + +--- + +## References + +- [Google Sign-In for iOS](https://developers.google.com/identity/sign-in/ios/start) +- [Google Sign-In for Android](https://developers.google.com/identity/sign-in/android/start) +- [@react-native-google-signin/google-signin](https://github.com/react-native-google-signin/google-signin) +- [Supabase Google Auth (React Native)](https://supabase.com/docs/guides/auth/social-login/auth-google?platform=react-native) + diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.desktop.tsx b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.desktop.tsx index 0f3241c3f836..e0bd1f30e04f 100644 --- a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.desktop.tsx +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.desktop.tsx @@ -36,7 +36,7 @@ export class OAuthPopup extends OAuthPopupBase { * Get OAuth redirect URL for Desktop platform. * * Starts localhost OAuth server and returns callback URL. - * Returns: http://127.0.0.1:{port}/oauth/callback + * Returns: http://127.0.0.1:{port}/oauth_callback_desktop */ static override async getRedirectUrl(): Promise { if ( diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx index 64b7417833c3..4b5e0b60ba73 100644 --- a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx @@ -1,25 +1,38 @@ /* eslint-disable spellcheck/spell-checker */ -import { GoogleSignin } from '@react-native-google-signin/google-signin'; import * as WebBrowser from 'expo-web-browser'; import { DEFAULT_NATIVE_OAUTH_METHOD, ENativeOAuthMethod, GOOGLE_OAUTH_CLIENT_IDS, + OAUTH_CALLBACK_NATIVE_PATH, OAUTH_TOKEN_KEY_ACCESS_TOKEN, OAUTH_TOKEN_KEY_REFRESH_TOKEN, } from '@onekeyhq/shared/src/consts/authConsts'; import { ONEKEY_APP_DEEP_LINK } from '@onekeyhq/shared/src/consts/deeplinkConsts'; +import { + GoogleSignInConfigure, + GoogleSignInConfigureIOS, +} from '@onekeyhq/shared/src/consts/googleSignConsts'; import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; import { OAuthPopupBase } from './OAuthPopupBase'; -import type { - INativeOAuthConfig, - IOAuthPopupOptions, - IOAuthPopupResult, -} from './types'; +import type { IOAuthPopupOptions, IOAuthPopupResult } from './types'; +import type { GoogleSignin as GoogleSigninType } from '@react-native-google-signin/google-signin'; + +// Lazy load GoogleSignin to avoid crash if native module is not available +async function getGoogleSignin(): Promise { + try { + const module = await import('@react-native-google-signin/google-signin'); + return module.GoogleSignin; + } catch (error) { + throw new OneKeyLocalError( + 'Google Sign-In is not available. Please use web browser authentication.', + ); + } +} // ============================================================================ // Native OAuth Popup Implementation @@ -42,12 +55,14 @@ export class OAuthPopup extends OAuthPopupBase { /** * Get OAuth redirect URL for native platforms. * - * Uses the deep link scheme: onekey-wallet://auth/callback + * Uses the deep link scheme: onekey-wallet://oauth_callback_native * Note: This is only used for WEB_BROWSER method. * GOOGLE_SIGNIN method doesn't need a redirect URL. */ static override getRedirectUrl(): Promise { - return Promise.resolve(`${ONEKEY_APP_DEEP_LINK}auth/callback`); + return Promise.resolve( + `${ONEKEY_APP_DEEP_LINK}${OAUTH_CALLBACK_NATIVE_PATH}`, + ); } /** @@ -105,23 +120,19 @@ export class OAuthPopup extends OAuthPopupBase { /** * Configure GoogleSignin with the provided options. + * Returns the GoogleSignin instance for further use. */ - private static configureGoogleSignin( - nativeConfig: INativeOAuthConfig | undefined, - ): void { - const configOptions: Parameters[0] = { - scopes: nativeConfig?.scopes ?? ['openid', 'profile', 'email'], - offlineAccess: true, - }; + private static async configureGoogleSignin(): Promise< + typeof GoogleSigninType + > { + const GoogleSignin = await getGoogleSignin(); - if (platformEnv.isNativeIOS) { - configOptions.iosClientId = GOOGLE_OAUTH_CLIENT_IDS.IOS; - } - if (platformEnv.isNativeAndroid) { - configOptions.webClientId = GOOGLE_OAUTH_CLIENT_IDS.ANDROID; - } + const configOptions = platformEnv.isNativeIOS + ? GoogleSignInConfigureIOS + : GoogleSignInConfigure; GoogleSignin.configure(configOptions); + return GoogleSignin; } /** @@ -137,14 +148,14 @@ export class OAuthPopup extends OAuthPopupBase { private static async openWithGoogleSignin( options: IOAuthPopupOptions, ): Promise { - const { client, nativeConfig, handleSessionPersistence } = options; + const { client, handleSessionPersistence } = options; if (!client) { throw new OneKeyLocalError('Supabase client is required'); } - // Configure GoogleSignin - OAuthPopup.configureGoogleSignin(nativeConfig); + // Configure GoogleSignin (lazy loaded) + const GoogleSignin = await OAuthPopup.configureGoogleSignin(); try { // Check if Google Play Services is available (Android only) diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/types.ts b/packages/kit/src/components/OneKeyAuth/OAuthPopup/types.ts index 866abb2a5ac9..43fc2d5864aa 100644 --- a/packages/kit/src/components/OneKeyAuth/OAuthPopup/types.ts +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/types.ts @@ -31,22 +31,6 @@ export type IOpenOAuthPopupOptions = { persistSession?: boolean; }; -/** - * OAuth configuration for Google sign-in (native iOS/Android). - * These values should match your Google Cloud Console OAuth 2.0 Client ID settings. - */ -export interface INativeOAuthConfig { - // Google OAuth Client ID for iOS - // Create this in Google Cloud Console > APIs & Services > Credentials > OAuth 2.0 Client IDs - // Application type: iOS - iosClientId?: string; - // Google OAuth Client ID for Android (Web client ID is used for Android) - // Application type: Web application - webClientId?: string; - // OAuth scopes to request - scopes?: string[]; -} - /** * Unified OAuth popup options for all platforms */ @@ -62,6 +46,4 @@ export interface IOAuthPopupOptions { handleSessionPersistence: ( params: IHandleOAuthSessionPersistenceParams, ) => Promise; - // Native-specific OAuth config (only used on native platform) - nativeConfig?: INativeOAuthConfig; } diff --git a/packages/shared/src/consts/authConsts.ts b/packages/shared/src/consts/authConsts.ts index 17eacc7ac885..a0770757f380 100644 --- a/packages/shared/src/consts/authConsts.ts +++ b/packages/shared/src/consts/authConsts.ts @@ -18,6 +18,11 @@ export const OAUTH_CALLBACK_DESKTOP_PATH = '/oauth_callback_desktop'; */ export const OAUTH_CALLBACK_WEB_PATH = '/oauth_callback_web/'; +/** + * OAuth callback path for native platforms (iOS/Android) + */ +export const OAUTH_CALLBACK_NATIVE_PATH = 'oauth_callback_native'; + // ============================================================================ // OAuth shared constants (OneKeyAuth) // ============================================================================ @@ -31,7 +36,7 @@ export enum EDesktopOAuthMethod { LOCALHOST_SERVER = 'LOCALHOST_SERVER', // Use in-app webview to handle OAuth - // Intercepts navigation to onekey-wallet://auth/callback + // Intercepts navigation to onekey-wallet://oauth_callback_native WEBVIEW = 'WEBVIEW', // Use system browser + deep link callback @@ -67,7 +72,7 @@ export enum ENativeOAuthMethod { // Fallback: Use expo-web-browser.openAuthSessionAsync // Opens in-app browser for OAuth, uses deep link callback - // Redirect URL: onekey-wallet://auth/callback + // Redirect URL: onekey-wallet://oauth_callback_native WEB_BROWSER = 'WEB_BROWSER', } diff --git a/packages/shared/src/consts/googleSignConsts.ts b/packages/shared/src/consts/googleSignConsts.ts index f636553e25bb..866c7332b160 100644 --- a/packages/shared/src/consts/googleSignConsts.ts +++ b/packages/shared/src/consts/googleSignConsts.ts @@ -1,12 +1,27 @@ /* eslint-disable spellcheck/spell-checker */ import platformEnv from '../platformEnv'; +import { GOOGLE_OAUTH_CLIENT_IDS } from './authConsts'; + export const GoogleSignInConfigure = { - scopes: ['https://www.googleapis.com/auth/drive.file'], + scopes: [ + 'openid', + 'profile', + 'email', + 'https://www.googleapis.com/auth/drive.file', + ], + offlineAccess: false, + webClientId: GOOGLE_OAUTH_CLIENT_IDS.ANDROID, + // webClientId: platformEnv.isDev // ? // DEVELOPER_ERROR: On local development this can happen due to incorrect app signing. Please test with a production-signed build. // '117481276073-fs7omuqsmvgtg6bci3ja1gvo03g0d984.apps.googleusercontent.com' // Dev // : '94391474021-ffaspa4ikjqpqvn5ndplqobvuvhnj8v3.apps.googleusercontent.com', // Pro // offlineAccess: true, +}; + +export const GoogleSignInConfigureIOS = { + scopes: ['openid', 'profile', 'email'], offlineAccess: false, + iosClientId: GOOGLE_OAUTH_CLIENT_IDS.IOS, }; From 5b1b70233f264f5718476b7abb0aba6a6d13b721 Mon Sep 17 00:00:00 2001 From: morizon Date: Mon, 22 Dec 2025 21:03:04 +0800 Subject: [PATCH 12/66] fix: ios google login --- apps/mobile/ios/OneKeyWallet/Info.plist | 8 +++++++ .../OAuthPopup/GOOGLE_SIGNIN_SETUP.md | 23 ++++++++++--------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/apps/mobile/ios/OneKeyWallet/Info.plist b/apps/mobile/ios/OneKeyWallet/Info.plist index 5851e789d28c..71bcbcf5b65f 100644 --- a/apps/mobile/ios/OneKeyWallet/Info.plist +++ b/apps/mobile/ios/OneKeyWallet/Info.plist @@ -33,6 +33,14 @@ ethereum + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.googleusercontent.apps.244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt + + CFBundleVersion 1 diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/GOOGLE_SIGNIN_SETUP.md b/packages/kit/src/components/OneKeyAuth/OAuthPopup/GOOGLE_SIGNIN_SETUP.md index effeaa098c8e..6e11d9f7f831 100644 --- a/packages/kit/src/components/OneKeyAuth/OAuthPopup/GOOGLE_SIGNIN_SETUP.md +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/GOOGLE_SIGNIN_SETUP.md @@ -54,17 +54,7 @@ cd apps/mobile/ios pod install ``` -### 3. Add GoogleService-Info.plist - -1. Go to [Firebase Console](https://console.firebase.google.com/) or [Google Cloud Console](https://console.cloud.google.com/) -2. Download `GoogleService-Info.plist` for your iOS app -3. Add it to the Xcode project: - - Open `apps/mobile/ios/OneKeyWallet.xcworkspace` in Xcode - - Drag `GoogleService-Info.plist` into the project navigator - - Ensure "Copy items if needed" is checked - - Add to target: `OneKeyWallet` - -### 4. Configure URL Scheme in Info.plist +### 3. Configure URL Scheme in Info.plist (Required) Add the reversed client ID as a URL scheme in `apps/mobile/ios/OneKeyWallet/Info.plist`: @@ -86,6 +76,17 @@ Add the reversed client ID as a URL scheme in `apps/mobile/ios/OneKeyWallet/Info > **Note**: The URL scheme is the iOS Client ID reversed. For `244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt.apps.googleusercontent.com`, use `com.googleusercontent.apps.244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt`. +### 4. GoogleService-Info.plist (Optional) + +`GoogleService-Info.plist` is **NOT required** for Google Sign-In alone. It's only needed if you use other Firebase services (Analytics, Crashlytics, etc.). + +If you do need it: +1. Go to [Firebase Console](https://console.firebase.google.com/) +2. Create a project or select existing one +3. Add an iOS app with your bundle ID +4. Download `GoogleService-Info.plist` +5. Add to Xcode project + ### 5. Verify Configuration The iOS Client ID in `packages/shared/src/consts/authConsts.ts` should match: From 86917c6835340701f8fc4ccbcfeacc3b7f52d301 Mon Sep 17 00:00:00 2001 From: morizon Date: Mon, 22 Dec 2025 22:12:10 +0800 Subject: [PATCH 13/66] fix: upgrade google sign in sdk --- .../OneKeyWallet.xcodeproj/project.pbxproj | 8 ++ apps/mobile/ios/Podfile | 3 + apps/mobile/ios/Podfile.lock | 96 +++++++++++++++---- apps/mobile/ios/PrivacyInfo.xcprivacy | 1 + apps/mobile/package.json | 2 +- apps/mobile/react-native.config.js | 2 +- yarn.lock | 12 +-- 7 files changed, 95 insertions(+), 29 deletions(-) diff --git a/apps/mobile/ios/OneKeyWallet.xcodeproj/project.pbxproj b/apps/mobile/ios/OneKeyWallet.xcodeproj/project.pbxproj index 7f454d5d5e28..39903630211b 100644 --- a/apps/mobile/ios/OneKeyWallet.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/OneKeyWallet.xcodeproj/project.pbxproj @@ -492,7 +492,11 @@ "${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GTMAppAuth/GTMAppAuth_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle", @@ -525,7 +529,11 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMAppAuth_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMSessionFetcher_Core_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle", diff --git a/apps/mobile/ios/Podfile b/apps/mobile/ios/Podfile index 5e0b00d3961b..073197752c1c 100644 --- a/apps/mobile/ios/Podfile +++ b/apps/mobile/ios/Podfile @@ -192,6 +192,9 @@ target 'OneKeyWallet' do # Enable modular headers for EMASCurl to fix SniConnect dependency pod 'EMASCurl', :modular_headers => true + # Manually add RNGoogleSignin pod (expo-module.config.json only includes the Expo adapter, not the TurboModule) + # pod 'RNGoogleSignin', :path => '../../../node_modules/@react-native-google-signin/google-signin' + post_install do |installer| react_native_post_install( installer, diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock index c98022e66966..b9ea6ef9f8ea 100644 --- a/apps/mobile/ios/Podfile.lock +++ b/apps/mobile/ios/Podfile.lock @@ -1,10 +1,14 @@ PODS: - - AppAuth (1.7.6): - - AppAuth/Core (= 1.7.6) - - AppAuth/ExternalUserAgent (= 1.7.6) - - AppAuth/Core (1.7.6) - - AppAuth/ExternalUserAgent (1.7.6): + - AppAuth (2.0.0): + - AppAuth/Core (= 2.0.0) + - AppAuth/ExternalUserAgent (= 2.0.0) + - AppAuth/Core (2.0.0) + - AppAuth/ExternalUserAgent (2.0.0): - AppAuth/Core + - AppCheckCore (11.2.0): + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) - BackgroundThread (1.1.11): - boost - DoubleConversion @@ -142,9 +146,9 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - ExpoAdapterGoogleSignIn (9.1.0): + - ExpoAdapterGoogleSignIn (16.1.0): - ExpoModulesCore - - GoogleSignIn (~> 6.2) + - GoogleSignIn (~> 9.0) - React-Core - ExpoAppleAuthentication (7.2.4): - ExpoModulesCore @@ -261,14 +265,24 @@ PODS: - FBLazyVector (0.81.5) - fmt (11.0.2) - glog (0.3.5) - - GoogleSignIn (6.2.4): - - AppAuth (~> 1.5) - - GTMAppAuth (~> 1.3) - - GTMSessionFetcher/Core (< 3.0, >= 1.1) - - GTMAppAuth (1.3.1): - - AppAuth/Core (~> 1.6) - - GTMSessionFetcher/Core (< 3.0, >= 1.5) - - GTMSessionFetcher/Core (2.3.0) + - GoogleSignIn (9.0.0): + - AppAuth (~> 2.0) + - AppCheckCore (~> 11.0) + - GTMAppAuth (~> 5.0) + - GTMSessionFetcher/Core (~> 3.3) + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMAppAuth (5.0.0): + - AppAuth/Core (~> 2.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3) + - GTMSessionFetcher/Core (3.5.0) - hermes-engine (0.81.5): - hermes-engine/Pre-built (= 0.81.5) - hermes-engine/Pre-built (0.81.5) @@ -412,6 +426,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga + - PromisesObjC (2.4.0) - PurchasesHybridCommon (14.2.0): - RevenueCat (= 5.32.0) - RCT-Folly (2024.11.18.00): @@ -3457,6 +3472,35 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga + - RNGoogleSignin (16.1.0): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - GoogleSignIn (~> 9.0) + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - RNImageCropPicker (0.50.0): - boost - DoubleConversion @@ -4085,6 +4129,7 @@ DEPENDENCIES: - RNDnsLookup (from `../../../node_modules/react-native-dns-lookup`) - RNFileLogger (from `../../../node_modules/react-native-file-logger`) - RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`) + - "RNGoogleSignin (from `../../../node_modules/@react-native-google-signin/google-signin`)" - RNImageCropPicker (from `../../../node_modules/react-native-image-crop-picker`) - "RNNotifee (from `../../../node_modules/@notifee/react-native`)" - RNPermissions (from `../../../node_modules/react-native-permissions`) @@ -4104,15 +4149,18 @@ SPEC REPOS: - EMASCurl https://github.com/CocoaPods/Specs.git: - AppAuth + - AppCheckCore - CocoaAsyncSocket - CocoaLumberjack - GoogleSignIn + - GoogleUtilities - GTMAppAuth - GTMSessionFetcher - libwebp - lottie-ios - MMKVCore - MultiplatformBleAdapter + - PromisesObjC - PurchasesHybridCommon - React-Codegen - RevenueCat @@ -4417,6 +4465,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native-file-logger" RNGestureHandler: :path: "../../../node_modules/react-native-gesture-handler" + RNGoogleSignin: + :path: "../../../node_modules/@react-native-google-signin/google-signin" RNImageCropPicker: :path: "../../../node_modules/react-native-image-crop-picker" RNNotifee: @@ -4443,7 +4493,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 + AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063 + AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f BackgroundThread: d6bedf61338886caa616744dc1e4aa77656dc285 BleUtils: 0fd18e5fd2732c89771dc79941c7320ac9c42015 boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 @@ -4458,7 +4509,7 @@ SPEC CHECKSUMS: EXImageLoader: 4d3d3284141f1a45006cc4d0844061c182daf7ee EXNotifications: 1afe5275cf399920480fc5a9008676749a767d19 Expo: 22c06b7185706d92688f6e943576198540836bc1 - ExpoAdapterGoogleSignIn: ddfb635edae1ea92679a18ea34cb6ef6dbd69f03 + ExpoAdapterGoogleSignIn: d37ffdf6129a723126c1ead19f8ba52a9ab49dc6 ExpoAppleAuthentication: 4d2e0c88a4463229760f1fbb9a937a810efb6863 ExpoAsset: d839c8eae8124470332408427327e8f88beb2dfd ExpoBlur: 2dd8f64aa31f5d405652c21d3deb2d2588b1852f @@ -4488,9 +4539,10 @@ SPEC CHECKSUMS: FBLazyVector: 5beb8028d5a2e75dd9634917f23e23d3a061d2aa fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - GoogleSignIn: 5651ce3a61e56ca864160e79b484cd9ed3f49b7a - GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd - GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 + GoogleSignIn: c7f09cfbc85a1abf69187be091997c317cc33b77 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238 + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172 ImageColors: 51cd79f7a9d2524b7a681c660b0a50574085563b JCoreRN: d985de509185b381c177fde4bb6bfc686089fdbd @@ -4503,6 +4555,7 @@ SPEC CHECKSUMS: MultiplatformBleAdapter: b1fddd0d499b96b607e00f0faa8e60648343dc1d NitroMmkv: ce1df9b9f0e06dfbde2455d863047e0411fceb6e NitroModules: 5bc319d441f4983894ea66b1d392c519536e6d23 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PurchasesHybridCommon: f1650b33e9a1dd04d5e8a40dfe757f39e63f935c RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: 5eb1d2eeff5fb91151e8a8eef45b6c7658b6c897 @@ -4601,6 +4654,7 @@ SPEC CHECKSUMS: RNDnsLookup: db4a89381b80ec1a5153088518d2c4f8e51f2521 RNFileLogger: 5f13e0115cf71f27c032195a55567f5b473db4e7 RNGestureHandler: 29bbca60c881e5243e219e3fe0016060002ed389 + RNGoogleSignin: 628d629e8dca39fc4ab70e30cf55bb3fc284f518 RNImageCropPicker: 882ced4c8ec0ce16b59456d935af91d9126cfd49 RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168 RNPermissions: 808e11d2ffe4d8ddea33d8304310a28f281b6a0a @@ -4624,6 +4678,6 @@ SPEC CHECKSUMS: Yoga: 728df40394d49f3f471688747cf558158b3a3bd1 ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 -PODFILE CHECKSUM: ea18296bb911f4fe6f7cbbe3550d227a64e69397 +PODFILE CHECKSUM: f6bfc90d74a6aa59c5d7ba0bb87843c1e8b77274 COCOAPODS: 1.16.2 diff --git a/apps/mobile/ios/PrivacyInfo.xcprivacy b/apps/mobile/ios/PrivacyInfo.xcprivacy index 2e0cd4040da4..4edd0b44dc45 100644 --- a/apps/mobile/ios/PrivacyInfo.xcprivacy +++ b/apps/mobile/ios/PrivacyInfo.xcprivacy @@ -29,6 +29,7 @@ NSPrivacyAccessedAPITypeReasons CA92.1 + C56D.1 diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 0614c9f15fa2..fbe33553f33d 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -57,7 +57,7 @@ "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/netinfo": "11.4.1", "@react-native-community/slider": "5.0.1", - "@react-native-google-signin/google-signin": "^9.1.0", + "@react-native-google-signin/google-signin": "16.1.0", "@reown/appkit-ethers5-react-native": "1.3.1", "@sentry/react-native": "6.22.0", "@shopify/flash-list": "2.2.0", diff --git a/apps/mobile/react-native.config.js b/apps/mobile/react-native.config.js index d596f1c3aedd..060a4faf0a47 100644 --- a/apps/mobile/react-native.config.js +++ b/apps/mobile/react-native.config.js @@ -3,7 +3,7 @@ module.exports = { dependencies: { '@react-native-google-signin/google-signin': { platforms: { - ios: null, + // ios: null, }, }, 'react-native-check-biometric-auth-changed': { diff --git a/yarn.lock b/yarn.lock index e4182394f464..463c366c5f4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7899,7 +7899,7 @@ __metadata: "@react-native-async-storage/async-storage": "npm:2.2.0" "@react-native-community/netinfo": "npm:11.4.1" "@react-native-community/slider": "npm:5.0.1" - "@react-native-google-signin/google-signin": "npm:^9.1.0" + "@react-native-google-signin/google-signin": "npm:16.1.0" "@react-native/metro-config": "npm:0.81.5" "@reown/appkit-ethers5-react-native": "npm:1.3.1" "@rozenite/expo-atlas-plugin": "npm:1.0.0-alpha.16" @@ -10376,17 +10376,17 @@ __metadata: languageName: node linkType: hard -"@react-native-google-signin/google-signin@npm:^9.1.0": - version: 9.1.0 - resolution: "@react-native-google-signin/google-signin@npm:9.1.0" +"@react-native-google-signin/google-signin@npm:16.1.0": + version: 16.1.0 + resolution: "@react-native-google-signin/google-signin@npm:16.1.0" peerDependencies: - expo: ">=47.0.0" + expo: ">=52.0.40" react: "*" react-native: "*" peerDependenciesMeta: expo: optional: true - checksum: 10/342bbbedbd2be3098cbc652779254bac395f13a261745dfc2092fd09d4dae1c47242023da1e80fdffb119778110abb2b6b22245291d7c522a15c4aee151c7ff5 + checksum: 10/bd7f2f4055a35660acfa3bc1153254cbdb40643e893bb0b0f973c78e4919d7be5ba6b9979cddd0d740b61957b6f64094465ed251a844b4345961cf3b1dfd3eeb languageName: node linkType: hard From 06890c1fa53fdb9d40bc5c012443a55c8f02bdb7 Mon Sep 17 00:00:00 2001 From: morizon Date: Mon, 22 Dec 2025 22:41:54 +0800 Subject: [PATCH 14/66] fix: lint --- .../OAuthPopup/OAuthPopup.native.tsx | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx index 4b5e0b60ba73..6a0ccc3c4e23 100644 --- a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx @@ -4,10 +4,10 @@ import * as WebBrowser from 'expo-web-browser'; import { DEFAULT_NATIVE_OAUTH_METHOD, ENativeOAuthMethod, - GOOGLE_OAUTH_CLIENT_IDS, OAUTH_CALLBACK_NATIVE_PATH, OAUTH_TOKEN_KEY_ACCESS_TOKEN, OAUTH_TOKEN_KEY_REFRESH_TOKEN, + ONEKEY_OAUTH_STATE_KEY, } from '@onekeyhq/shared/src/consts/authConsts'; import { ONEKEY_APP_DEEP_LINK } from '@onekeyhq/shared/src/consts/deeplinkConsts'; import { @@ -248,7 +248,7 @@ export class OAuthPopup extends OAuthPopupBase { * Open OAuth using expo-web-browser.openAuthSessionAsync. * * This is the fallback method that opens an in-app browser. - * Extracts tokens from the callback URL when authentication is complete. + * Uses PKCE flow: extracts authorization code and exchanges for session. */ private static async openWithWebBrowser( options: IOAuthPopupOptions, @@ -257,6 +257,7 @@ export class OAuthPopup extends OAuthPopupBase { authUrl, redirectTo: redirectToFromOptions, handleSessionPersistence, + client, } = options; if (!authUrl) { @@ -269,8 +270,18 @@ export class OAuthPopup extends OAuthPopupBase { ); } + if (!client) { + throw new OneKeyLocalError( + 'Supabase client is required for WebBrowser method', + ); + } + const redirectTo = redirectToFromOptions; + // Parse expected states for validation + const { expectedState, expectedOneKeyState } = + OAuthPopup.parseExpectedStates(authUrl); + // Open in-app browser for OAuth const browserResult = await WebBrowser.openAuthSessionAsync( authUrl, @@ -282,7 +293,42 @@ export class OAuthPopup extends OAuthPopupBase { ); if (browserResult.type === 'success' && browserResult.url) { - // Extract tokens from the callback URL + const url = new URL(browserResult.url); + + // Check for error in callback + const error = + url.searchParams.get('error') || + url.searchParams.get('error_description'); + if (error) { + throw new OneKeyLocalError(error); + } + + // PKCE flow: extract authorization code + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + const oneKeyState = url.searchParams.get(ONEKEY_OAUTH_STATE_KEY); + + if (code) { + // Validate states + OAuthPopup.validateOneKeyState(expectedOneKeyState, oneKeyState); + OAuthPopup.validateSupabaseState(expectedState, state); + + // Exchange code for session using PKCE + const { accessToken, refreshToken } = + await OAuthPopup.exchangeCodeForSession(client, code); + + await handleSessionPersistence({ + accessToken, + refreshToken, + }); + + return { + success: true, + session: { accessToken, refreshToken }, + }; + } + + // Fallback: try to extract tokens directly from URL (implicit flow) const { accessToken, refreshToken } = OAuthPopup.parseCallbackUrl( browserResult.url, ); From faabbeb9e3ced535be1a22cb478535af5aa8c470 Mon Sep 17 00:00:00 2001 From: morizon Date: Tue, 23 Dec 2025 18:33:48 +0800 Subject: [PATCH 15/66] feat: add Apple Sign-In setup guide - Introduced a comprehensive guide for configuring Apple Sign-In with Supabase for the OneKey app, focusing on web platform setup. - Documented prerequisites, local development testing, and detailed steps for Apple Developer Console and Supabase configuration. - Included troubleshooting tips and security considerations to enhance user understanding and implementation of Apple Sign-In. - Updated OAuthPopup components to improve callback URL validation and nonce generation for enhanced security during authentication. --- .../GoogleDriveBackupProvider.ts | 24 +- .../OAuthPopup/APPLE_SIGNIN_SETUP.md | 461 ++++++++++++++++++ .../OneKeyAuth/OAuthPopup/OAuthPopup.ext.tsx | 20 +- .../OAuthPopup/OAuthPopup.native.tsx | 31 +- .../components/OneKeyAuth/OAuthPopup/index.ts | 1 - .../Components/stories/OneKeyIDGallery.tsx | 40 +- packages/shared/src/cloudfs/index.android.ts | 13 +- packages/shared/src/consts/authConsts.ts | 1 + .../GoogleDriveStorage.android.ts | 47 +- 9 files changed, 596 insertions(+), 42 deletions(-) create mode 100644 packages/kit/src/components/OneKeyAuth/OAuthPopup/APPLE_SIGNIN_SETUP.md diff --git a/packages/kit-bg/src/services/ServiceCloudBackupV2/backupProviders/GoogleDriveBackupProvider.ts b/packages/kit-bg/src/services/ServiceCloudBackupV2/backupProviders/GoogleDriveBackupProvider.ts index 259da2ab83d6..f4bf9a726192 100644 --- a/packages/kit-bg/src/services/ServiceCloudBackupV2/backupProviders/GoogleDriveBackupProvider.ts +++ b/packages/kit-bg/src/services/ServiceCloudBackupV2/backupProviders/GoogleDriveBackupProvider.ts @@ -142,18 +142,18 @@ export class GoogleDriveBackupProvider implements IOneKeyBackupProvider { // isCloudFsAvailable = await RNCloudFs.isAvailable(); GoogleSignin.configure(GoogleSignInConfigure); - const isSignedIn = await GoogleSignin.isSignedIn(); - - // if (!isSignedIn) { - // await GoogleSignin.signInSilently(); - // isSignedIn = await GoogleSignin.isSignedIn(); - // } - - if (isSignedIn) { - userInfo = await GoogleSignin.getCurrentUser(); - email = userInfo?.user?.email || ''; - - // await RNCloudFs.loginIfNeeded(); + const isPreviousSignedIn = GoogleSignin.hasPreviousSignIn(); + if (isPreviousSignedIn) { + try { + const response = await GoogleSignin.signInSilently(); + if (response.type === 'success') { + userInfo = GoogleSignin.getCurrentUser(); + email = userInfo?.user?.email || ''; + } + } catch (error) { + // signInSilently failed, return empty account info + console.log('GoogleSignin.signInSilently failed:', error); + } } } diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/APPLE_SIGNIN_SETUP.md b/packages/kit/src/components/OneKeyAuth/OAuthPopup/APPLE_SIGNIN_SETUP.md new file mode 100644 index 000000000000..890daeca4e9f --- /dev/null +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/APPLE_SIGNIN_SETUP.md @@ -0,0 +1,461 @@ +# Apple Sign-In Setup Guide + +This document describes how to configure Apple Sign-In for the OneKey app, with a focus on web platform setup. + +## Overview + +OneKey uses Apple Sign-In with Supabase for authentication. The web platform uses Supabase's OAuth flow: + +1. User clicks "Sign in with Apple" +2. Supabase generates OAuth URL with PKCE flow +3. User authenticates with Apple in popup window +4. Apple redirects to Supabase with authorization code +5. Supabase exchanges code for session +6. OneKey receives access/refresh tokens + +## Prerequisites + +Before starting, ensure you have: + +1. **Apple Developer Program Membership** ($99/年) - https://developer.apple.com/programs/enroll/ + > ⚠️ **免费开发者账户不支持 Sign in with Apple!** 你需要付费会员才能: + > - 创建 App ID 和 Services ID + > - 生成私钥 (.p8 文件) + > - 配置 OAuth 回调 URL + +2. Access to **Supabase Dashboard** (https://supabase.com/dashboard) + +## Local Development Testing + +**可以在本地测试 Apple Sign-In!** 原理如下: + +``` +┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ ┌─────────────┐ +│ localhost │────>│ Supabase (HTTPS)│────>│ Apple │────>│ Supabase │ +│ :3000 │ │ OAuth URL │ │ Sign-In │ │ Callback │ +└─────────────┘ └──────────────────┘ └─────────────┘ └─────────────┘ + │ + ▼ + ┌─────────────────┐ + │ localhost:3000 │ + │ /oauth_callback│ + └─────────────────┘ +``` + +**关键点**: +- Apple OAuth 的 callback 先到达 Supabase(HTTPS),不是直接到 localhost +- Supabase 再重定向到你的 localhost +- 所以 **localhost 不需要 HTTPS** + +### 本地开发配置步骤 + +1. **Apple Developer Console - Services ID 配置**: + - **Domains**: 添加 `localhost` + - **Return URLs**: 保持 Supabase callback URL(不需要改) + ``` + https://zvxscjkvkjepbrjncvzt.supabase.co/auth/v1/callback + ``` + +2. **Supabase Dashboard - Redirect URLs**: + - 添加本地开发 URL: + ``` + http://localhost:3000/oauth_callback_web/ + http://127.0.0.1:3000/oauth_callback_web/ + ``` + +3. **启动本地开发服务器**: + ```bash + yarn app:web + ``` + +4. **访问** `http://localhost:3000` 并测试 Apple 登录 + +> **注意**: 如果端口不是 3000,请相应修改 Supabase 的 Redirect URLs + +--- + +## Part 1: Apple Developer Console Configuration + +### Step 1: Create an App ID + +1. Go to [Apple Developer Console](https://developer.apple.com/account) +2. Navigate to **Certificates, Identifiers & Profiles** → **Identifiers** +3. Click the **+** button to create a new identifier +4. Select **App IDs** → **Continue** +5. Select **App** type → **Continue** +6. Fill in the details: + - **Description**: OneKey Wallet + - **Bundle ID**: `so.onekey.wallet.desktop` (or your bundle ID) +7. Under **Capabilities**, check **Sign in with Apple** +8. Click **Continue** → **Register** + +### Step 2: Create a Services ID (Required for Web) + +1. In **Identifiers**, click **+** again +2. Select **Services IDs** → **Continue** +3. Fill in the details: + - **Description**: OneKey Web Login + - **Identifier**: `so.onekey.wallet.web` (this will be your `client_id`) +4. Click **Continue** → **Register** +5. **Click on the newly created Services ID** to configure it +6. Check **Sign in with Apple** → Click **Configure** +7. Configure the Web Authentication: + - **Primary App ID**: Select your App ID from Step 1 + - **Domains and Subdomains**: Add your domains, e.g.: + ``` + app.onekey.so + localhost + ``` + - **Return URLs** (Redirect URIs): Add: + ``` + https://zvxscjkvkjepbrjncvzt.supabase.co/auth/v1/callback + ``` + > **Note**: Replace with your Supabase project URL. Find it at: + > Supabase Dashboard → Project Settings → API → Project URL +8. Click **Save** → **Continue** → **Save** + +### Step 3: Create a Private Key + +1. Navigate to **Keys** in the left sidebar +2. Click **+** to create a new key +3. Fill in: + - **Key Name**: OneKey Sign In Key +4. Check **Sign in with Apple** → Click **Configure** +5. Select your **Primary App ID** from Step 1 +6. Click **Save** → **Continue** → **Register** +7. **Download the key file** (`.p8` file) - you can only download this once! +8. Note down the **Key ID** (10-character string) + +### Step 4: Note Your Team ID + +1. Go to [Apple Developer Account](https://developer.apple.com/account) +2. Your **Team ID** is shown in the top right, or under **Membership Details** + +--- + +## Part 2: Supabase Configuration + +### Step 1: Configure Apple Provider + +1. Go to [Supabase Dashboard](https://supabase.com/dashboard) +2. Select your project +3. Navigate to **Authentication** → **Providers** +4. Find **Apple** and click to expand +5. Toggle **Enable Sign in with Apple** +6. Fill in the configuration: + +| Field | Value | Description | +|-------|-------|-------------| +| **Client ID (Services ID)** | `so.onekey.wallet.web` | The Services ID identifier from Step 2 | +| **Secret Key** | `-----BEGIN PRIVATE KEY-----...` | Contents of the `.p8` file downloaded in Step 3 | +| **Key ID** | `ABC1234567` | The 10-character Key ID from Step 3 | +| **Team ID** | `TEAM123456` | Your 10-character Team ID from Step 4 | + +7. Click **Save** + +### Step 2: Configure Redirect URLs + +1. Navigate to **Authentication** → **URL Configuration** +2. Add your application's OAuth callback URLs to **Redirect URLs**: + +For Web Platform: +``` +https://app.onekey.so/oauth_callback_web/ +http://localhost:3000/oauth_callback_web/ +``` + +For Desktop Platform (localhost server): +``` +http://localhost:3846/oauth_callback_desktop +http://127.0.0.1:3846/oauth_callback_desktop +``` + +> **Note**: The trailing slash matters! Match exactly what your application sends. + +### Step 3: Verify Configuration + +Test the setup: +1. Go to **Authentication** → **Providers** → **Apple** +2. Check that all fields are filled correctly +3. Ensure no error messages are displayed + +--- + +## Part 3: Web Platform Implementation + +The web platform OAuth is already implemented in the codebase. Here's how it works: + +### OAuth Flow + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ +│ OneKey │────>│ Supabase │────>│ Apple │────>│ Callback │ +│ Web App │ │ OAuth URL │ │ Sign-In │ │ Handler │ +└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘ + │ │ + │ │ + └──────────────────── Session Tokens ──────────────────────────┘ +``` + +### Key Files + +| File | Purpose | +|------|---------| +| `OAuthPopup.tsx` | Web popup OAuth implementation | +| `OAuthPopupBase.ts` | Shared OAuth utilities | +| `useSupabaseAuth.tsx` | React hook for OAuth sign-in | +| `authConsts.ts` | OAuth configuration constants | + +### Usage in Code + +```typescript +import { useSupabaseAuth } from '@onekeyhq/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth'; + +function LoginComponent() { + const { signInWithApple } = useSupabaseAuth(); + + const handleAppleLogin = async () => { + try { + const result = await signInWithApple({ + persistSession: true, // Save session to storage + }); + + if (result.success) { + console.log('Logged in with Apple!'); + console.log('Access Token:', result.session?.accessToken); + } + } catch (error) { + console.error('Apple Sign-In failed:', error); + } + }; + + return ( + + ); +} +``` + +### Redirect URL Configuration + +The redirect URL is defined in `authConsts.ts`: + +```typescript +export const OAUTH_CALLBACK_WEB_PATH = '/oauth_callback_web/'; +``` + +The full redirect URL is constructed as: +``` +${window.location.origin}/oauth_callback_web/ +// Example: https://app.onekey.so/oauth_callback_web/ +``` + +--- + +## Part 4: Desktop & Extension Configuration + +### Desktop (Electron) + +Desktop uses a localhost HTTP server for OAuth callback: + +1. The redirect URL is: `http://localhost:3846/oauth_callback_desktop` +2. This is already configured in `OAuthPopup.desktop.tsx` +3. Make sure to add this URL in: + - Apple Developer Console → Services ID → Return URLs + - Supabase Dashboard → URL Configuration → Redirect URLs + +### Browser Extension + +Extensions use `chrome.identity.launchWebAuthFlow`: + +1. The redirect URL format is: `https://.chromiumapp.org` +2. This is automatically handled by Chrome +3. Add the redirect URL in: + - Apple Developer Console → Services ID → Return URLs + - Supabase Dashboard → URL Configuration → Redirect URLs + +> **Note**: You need to know your extension ID. For development, use a stable extension ID by configuring `key` in `manifest.json`. + +--- + +## Part 5: iOS & Android Configuration + +### iOS Native + +For iOS, you can use native Apple Sign-In: + +1. Enable "Sign in with Apple" capability in Xcode: + - Select your target → **Signing & Capabilities** + - Click **+ Capability** → **Sign in with Apple** + +2. The bundle ID must match your App ID from Apple Developer Console + +3. Native sign-in returns an ID token that can be exchanged with Supabase: + +```typescript +// Example using @invertase/react-native-apple-authentication +import appleAuth from '@invertase/react-native-apple-authentication'; + +const appleAuthResult = await appleAuth.performRequest({ + requestedOperation: appleAuth.Operation.LOGIN, + requestedScopes: [appleAuth.Scope.EMAIL, appleAuth.Scope.FULL_NAME], +}); + +// Exchange Apple ID token with Supabase +const { data, error } = await supabase.auth.signInWithIdToken({ + provider: 'apple', + token: appleAuthResult.identityToken, +}); +``` + +### Android - 使用 Web OAuth 流程 + +**Android 可以使用 Apple 登录!** Apple 官方支持通过 Web OAuth 流程在 Android 上实现 Sign in with Apple。 + +参考: [Apple 官方文档 - 在网站和其他平台上使用 Sign in with Apple](https://developer.apple.com/cn/sign-in-with-apple/usage-guidelines-for-websites-and-other-platforms/) + +#### 实现原理 + +Android 没有原生的 Apple Sign-In SDK,但可以通过 **expo-web-browser** 打开 Web OAuth 流程: + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ +│ Android │────>│ In-App │────>│ Apple │────>│ Deep Link │ +│ App │ │ Browser │ │ Sign-In │ │ Callback │ +└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘ +``` + +#### 当前实现 + +在 `OAuthPopup.native.tsx` 中: +- `signInWithApple` 会自动使用 `openWithWebBrowser` 方法 +- 使用 `expo-web-browser.openAuthSessionAsync` 打开 Apple 登录页面 +- 回调通过 deep link `onekey-wallet://oauth_callback_native` 返回 App + +#### 配置要求 + +1. **Apple Developer Console**: + - Services ID 的 **Return URLs** 需要添加 Supabase callback: + ``` + https://zvxscjkvkjepbrjncvzt.supabase.co/auth/v1/callback + ``` + +2. **Supabase Dashboard**: + - **Redirect URLs** 需要添加 deep link: + ``` + onekey-wallet://oauth_callback_native + ``` + +3. **Android App**: + - 确保 `onekey-wallet://` deep link scheme 已正确配置 + - 在 `AndroidManifest.xml` 中配置 intent filter + +#### 代码使用 + +```typescript +// Android 上调用 signInWithApple 会自动使用 WebBrowser 流程 +const { signInWithApple } = useSupabaseAuth(); + +const result = await signInWithApple({ persistSession: true }); +``` + +#### 注意事项 + +| 对比项 | iOS | Android | +|-------|-----|---------| +| 登录方式 | 可用原生 SDK 或 Web 流程 | 只能用 Web 流程 | +| 用户体验 | 原生弹窗,体验更好 | 打开内置浏览器 | +| Face ID / 指纹 | 支持 | 需要输入 Apple ID 密码 | +| 配置复杂度 | 需要 Xcode 配置 | 只需 Supabase 配置 | + +--- + +## Troubleshooting + +### "Invalid client_id" + +**Cause**: The Services ID doesn't match or isn't configured correctly. + +**Solution**: +1. Verify the Services ID in Supabase matches exactly (case-sensitive) +2. Ensure the Services ID has "Sign in with Apple" enabled +3. Check the domain/return URL configuration + +### "redirect_uri_mismatch" + +**Cause**: The redirect URL doesn't match Apple's configuration. + +**Solution**: +1. Check the Return URL in Apple Developer Console +2. Ensure the URL in Supabase matches exactly (including trailing slash) +3. Verify the domain is listed in the Services ID configuration + +### "invalid_grant" or Token Exchange Fails + +**Cause**: The private key or team configuration is incorrect. + +**Solution**: +1. Re-download the `.p8` private key +2. Verify the Key ID matches (10 characters) +3. Verify the Team ID matches (10 characters) +4. Ensure the private key content includes the full `-----BEGIN PRIVATE KEY-----` header + +### Popup Blocked + +**Cause**: Browser blocking popup window. + +**Solution**: +1. User needs to allow popups for the site +2. Ensure the popup is triggered by user action (click handler) + +### "User cancelled" Error + +**Cause**: User closed the Apple Sign-In popup without completing authentication. + +**Solution**: This is expected behavior - handle gracefully in UI. + +--- + +## Security Considerations + +1. **Never expose the private key** (`.p8` file) in client-side code +2. **Use HTTPS** for all production redirect URLs +3. **Validate state parameters** to prevent CSRF attacks (already implemented) +4. **Use PKCE flow** for enhanced security (already enabled) + +--- + +## Quick Reference: Required URLs + +### Apple Developer Console - Services ID Configuration + +**Domains**: +``` +app.onekey.so +localhost (for development) +``` + +**Return URLs**: +``` +https://zvxscjkvkjepbrjncvzt.supabase.co/auth/v1/callback +``` + +### Supabase - Redirect URLs + +``` +https://app.onekey.so/oauth_callback_web/ +http://localhost:3000/oauth_callback_web/ +http://localhost:3846/oauth_callback_desktop +http://127.0.0.1:3846/oauth_callback_desktop +https://.chromiumapp.org +``` + +--- + +## Related Documentation + +- [Supabase Apple OAuth Guide](https://supabase.com/docs/guides/auth/social-login/auth-apple) +- [Apple Sign in with Apple Documentation](https://developer.apple.com/sign-in-with-apple/) +- [Apple REST API for Sign in with Apple](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api) + diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.ext.tsx b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.ext.tsx index 7e8a4bdcda9a..8779970deaf4 100644 --- a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.ext.tsx +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.ext.tsx @@ -207,9 +207,24 @@ export class OAuthPopup extends OAuthPopupBase { } const url = new URL(callbackUrl); + const expectedUrl = new URL(redirectUrl); - if (!callbackUrl.startsWith(redirectUrl)) { - throw new OneKeyLocalError('Invalid OAuth redirect URL'); + // Extract onekey_oauth_state from both URLs for comparison + const oneKeyState = url.searchParams.get(ONEKEY_OAUTH_STATE_KEY); + const expectedOneKeyState = expectedUrl.searchParams.get( + ONEKEY_OAUTH_STATE_KEY, + ); + + // Compare origin, pathname, and onekey_oauth_state + // Query params order may differ, so we compare them separately + if ( + url.origin !== expectedUrl.origin || + url.pathname !== expectedUrl.pathname || + oneKeyState !== expectedOneKeyState + ) { + throw new OneKeyLocalError( + 'OAuth callback URL does not match expected redirect URL', + ); } const error = @@ -221,7 +236,6 @@ export class OAuthPopup extends OAuthPopupBase { const code = url.searchParams.get('code'); const state = url.searchParams.get('state'); - const oneKeyState = url.searchParams.get(ONEKEY_OAUTH_STATE_KEY); if (!code) { throw new OneKeyLocalError('Authorization code is missing'); diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx index 6a0ccc3c4e23..c6ab280d6377 100644 --- a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx @@ -1,4 +1,5 @@ /* eslint-disable spellcheck/spell-checker */ +import * as Crypto from 'expo-crypto'; import * as WebBrowser from 'expo-web-browser'; import { @@ -99,6 +100,23 @@ export class OAuthPopup extends OAuthPopupBase { // ============ Private Methods - GoogleSignin ============ + /** + * Generate a random nonce and its SHA-256 hash. + * The hash is passed to Google Sign-In, and the raw nonce to Supabase. + * This is required for iOS where the ID token contains a nonce. + */ + private static async generateNonce(): Promise<{ + rawNonce: string; + hashedNonce: string; + }> { + const rawNonce = Crypto.randomUUID(); + const hashedNonce = await Crypto.digestStringAsync( + Crypto.CryptoDigestAlgorithm.SHA256, + rawNonce, + ); + return { rawNonce, hashedNonce }; + } + /** * Check if error indicates GoogleSignin is not properly configured * and we should fall back to WebBrowser. @@ -157,6 +175,10 @@ export class OAuthPopup extends OAuthPopupBase { // Configure GoogleSignin (lazy loaded) const GoogleSignin = await OAuthPopup.configureGoogleSignin(); + // Generate nonce for security validation (required for iOS) + // The hashed nonce is passed to Google, raw nonce is passed to Supabase + const { rawNonce, hashedNonce } = await OAuthPopup.generateNonce(); + try { // Check if Google Play Services is available (Android only) if (platformEnv.isNativeAndroid) { @@ -165,12 +187,15 @@ export class OAuthPopup extends OAuthPopupBase { }); } - // Perform Google Sign-In + // Perform Google Sign-In with hashed nonce // The signIn() method returns different types based on library version: // - v9+: SignInResponse with { type: 'success' | 'cancelled', data?: User } // - older: User directly // We handle both cases for compatibility - const signInResult = await GoogleSignin.signIn(); + // Note: nonce is supported by the native SDK but not typed in current library version + const signInResult = await GoogleSignin.signIn({ + nonce: hashedNonce, + } as Parameters[0]); // Extract idToken - handle both v9+ and older API // v9+: signInResult may have .type and .data properties @@ -205,9 +230,11 @@ export class OAuthPopup extends OAuthPopupBase { // Exchange Google ID token for Supabase session // Per Supabase docs: https://supabase.com/docs/guides/auth/social-login/auth-google + // The raw nonce must be passed to Supabase to validate against the hashed nonce in the ID token const { data, error } = await client.auth.signInWithIdToken({ provider: 'google', token: idToken, + nonce: rawNonce, }); if (error) { diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/index.ts b/packages/kit/src/components/OneKeyAuth/OAuthPopup/index.ts index ec179b37ef4e..ffa621164b75 100644 --- a/packages/kit/src/components/OneKeyAuth/OAuthPopup/index.ts +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/index.ts @@ -10,7 +10,6 @@ export { OAuthPopup } from './OAuthPopup'; // Re-export types export type { IHandleOAuthSessionPersistenceParams, - INativeOAuthConfig, IOAuthPopupOptions, IOAuthPopupResult, IOpenOAuthPopupOptions, diff --git a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx index 2d71f5d72683..60dbaaed30d9 100644 --- a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx +++ b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx @@ -17,6 +17,10 @@ import { import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; import { useSupabaseAuth } from '@onekeyhq/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth'; import { useOneKeyAuth } from '@onekeyhq/kit/src/components/OneKeyAuth/useOneKeyAuth'; +import { + SUPABASE_PROJECT_URL, + SUPABASE_PUBLIC_API_KEY, +} from '@onekeyhq/shared/src/consts/authConsts'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; import { formatDate } from '@onekeyhq/shared/src/utils/dateUtils'; import stringUtils from '@onekeyhq/shared/src/utils/stringUtils'; @@ -127,6 +131,33 @@ function OneKeyIDApiTests() { + + Supabase Config (Debug) + + + + + PROJECT_URL: + + + {SUPABASE_PROJECT_URL || '(empty)'} + + + + + PUBLIC_API_KEY: + + + {SUPABASE_PUBLIC_API_KEY || '(empty)'} + + + + OAuth Sign In @@ -177,7 +208,14 @@ function OneKeyIDApiTests() { try { setLoading('apple'); const result = await signInWithApple({ persistSession }); - if (result.success) { + if (result.success && result.session?.accessToken) { + // Set access token + setAccessToken(result.session.accessToken); + // Decode JWT token + const decoded = stringUtils.decodeJWT( + result.session.accessToken, + ); + setDecodedToken(decoded); Toast.success({ title: 'Apple Sign In Success', message: 'You are now signed in with Apple', diff --git a/packages/shared/src/cloudfs/index.android.ts b/packages/shared/src/cloudfs/index.android.ts index bf77568ecddc..a22cc0bd21c4 100644 --- a/packages/shared/src/cloudfs/index.android.ts +++ b/packages/shared/src/cloudfs/index.android.ts @@ -18,15 +18,20 @@ export async function isAvailable(): Promise { export async function loginIfNeeded( showSignInDialog: boolean, ): Promise { - const signedIn = await GoogleSignin.isSignedIn(); - if (signedIn) { + const hasPreviousSignIn = GoogleSignin.hasPreviousSignIn(); + if (hasPreviousSignIn) { try { - return await RNCloudFs.loginIfNeeded(); + GoogleSignin.configure(GoogleSignInConfigure); + const response = await GoogleSignin.signInSilently(); + if (response.type === 'success') { + return await RNCloudFs.loginIfNeeded(); + } } catch (error) { // debugLogger.cloudBackup.error(error); return Promise.resolve(false); } - } else if (showSignInDialog) { + } + if (showSignInDialog) { GoogleSignin.configure(GoogleSignInConfigure); await GoogleSignin.signIn(); return RNCloudFs.loginIfNeeded(); diff --git a/packages/shared/src/consts/authConsts.ts b/packages/shared/src/consts/authConsts.ts index a0770757f380..a90d48d5ce3c 100644 --- a/packages/shared/src/consts/authConsts.ts +++ b/packages/shared/src/consts/authConsts.ts @@ -166,3 +166,4 @@ export const SUPABASE_PUBLIC_API_KEY = IS_DEV // Supabase DOCS // - https://supabase.com/docs/guides/auth/social-login/auth-google +// - https://react-native-google-signin.github.io/docs/setting-up/ios diff --git a/packages/shared/src/storage/GoogleDriveStorage/GoogleDriveStorage.android.ts b/packages/shared/src/storage/GoogleDriveStorage/GoogleDriveStorage.android.ts index f14a77e6a483..e85ace8cbdd8 100644 --- a/packages/shared/src/storage/GoogleDriveStorage/GoogleDriveStorage.android.ts +++ b/packages/shared/src/storage/GoogleDriveStorage/GoogleDriveStorage.android.ts @@ -72,10 +72,18 @@ export class GoogleDriveStorage { return false; } // Check if Google Play Services is available - const signedIn = await GoogleSignin.isSignedIn(); - if (signedIn) { - await RNCloudFs.loginIfNeeded(); - return true; + const hasPreviousSignedIn = GoogleSignin.hasPreviousSignIn(); + if (hasPreviousSignedIn) { + try { + const response = await GoogleSignin.signInSilently(); + if (response.type === 'success') { + await RNCloudFs.loginIfNeeded(); + return true; + } + } catch (error) { + console.warn('Google Drive availability check failed:', error); + return false; + } } return false; } catch (error) { @@ -90,13 +98,15 @@ export class GoogleDriveStorage { showSignInDialog: boolean; }): Promise { GoogleSignin.configure(GoogleSignInConfigure); - const signedIn = await GoogleSignin.isSignedIn(); - if (!signedIn) { + const hasPreviousSignedIn = GoogleSignin.hasPreviousSignIn(); + if (!hasPreviousSignedIn) { if (showSignInDialog) { await GoogleSignin.signIn(); } else { await GoogleSignin.signInSilently(); } + } else { + await GoogleSignin.signInSilently(); } return RNCloudFs.loginIfNeeded(); } @@ -117,19 +127,18 @@ export class GoogleDriveStorage { if (!platformEnv.isNativeAndroid) { return false; } - return GoogleSignin.isSignedIn(); + return GoogleSignin.hasPreviousSignIn(); } async getUserInfo(): Promise { await this.loginIfNeeded({ showSignInDialog: false }); - const signedIn = await GoogleSignin.isSignedIn(); - if (!signedIn) { + const hasPreviousSignedIn = GoogleSignin.hasPreviousSignIn(); + if (!hasPreviousSignedIn) { return null; } - const userInfo: IGoogleUserInfo | null = - await GoogleSignin.getCurrentUser(); + const userInfo: IGoogleUserInfo | null = GoogleSignin.getCurrentUser(); return userInfo; } @@ -140,8 +149,8 @@ export class GoogleDriveStorage { const { fileName, content } = params; // Ensure user is signed in - const signedIn = await GoogleSignin.isSignedIn(); - if (!signedIn) { + const hasPreviousSignedIn = GoogleSignin.hasPreviousSignIn(); + if (!hasPreviousSignedIn) { throw new OneKeyLocalError( 'Not signed in to Google. Please sign in first.', ); @@ -194,8 +203,8 @@ export class GoogleDriveStorage { const { fileId } = params; // Ensure user is signed in - const signedIn = await GoogleSignin.isSignedIn(); - if (!signedIn) { + const hasPreviousSignedIn = GoogleSignin.hasPreviousSignIn(); + if (!hasPreviousSignedIn) { throw new OneKeyLocalError( 'Not signed in to Google. Please sign in first.', ); @@ -217,8 +226,8 @@ export class GoogleDriveStorage { const { fileId } = params; // Ensure user is signed in - const signedIn = await GoogleSignin.isSignedIn(); - if (!signedIn) { + const hasPreviousSignedIn = GoogleSignin.hasPreviousSignIn(); + if (!hasPreviousSignedIn) { throw new OneKeyLocalError( 'Not signed in to Google. Please sign in first.', ); @@ -232,8 +241,8 @@ export class GoogleDriveStorage { async listFiles(): Promise<{ files: IGoogleDriveFile[] }> { // Ensure user is signed in - const signedIn = await GoogleSignin.isSignedIn(); - if (!signedIn) { + const hasPreviousSignedIn = GoogleSignin.hasPreviousSignIn(); + if (!hasPreviousSignedIn) { throw new OneKeyLocalError( 'Not signed in to Google. Please sign in first.', ); From d443a0f64be0308db5237c0bdde1b7235b176a7d Mon Sep 17 00:00:00 2001 From: morizon Date: Wed, 24 Dec 2025 20:14:42 +0800 Subject: [PATCH 16/66] apple login --- .../OAuthPopup/OAuthPopup.native.tsx | 358 +++++++++++++++--- .../components/OneKeyAuth/OAuthPopup/types.ts | 2 + .../OneKeyAuth/supabase/useSupabaseAuth.tsx | 28 ++ .../Components/stories/CryptoGallery.tsx | 151 ++++++++ .../src/appCrypto/cryptoSubtlePolyfill.js | 100 +++++ packages/shared/src/consts/authConsts.ts | 13 +- .../shared/src/polyfills/polyfillsPlatform.js | 9 + 7 files changed, 607 insertions(+), 54 deletions(-) create mode 100644 packages/shared/src/appCrypto/cryptoSubtlePolyfill.js diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx index c6ab280d6377..e6837e1ae770 100644 --- a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx @@ -1,16 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable spellcheck/spell-checker */ +import * as AppleAuthentication from 'expo-apple-authentication'; import * as Crypto from 'expo-crypto'; import * as WebBrowser from 'expo-web-browser'; +import { Dialog } from '@onekeyhq/components'; import { + APPLE_SIGNIN_USE_NONCE, DEFAULT_NATIVE_OAUTH_METHOD, ENativeOAuthMethod, OAUTH_CALLBACK_NATIVE_PATH, - OAUTH_TOKEN_KEY_ACCESS_TOKEN, - OAUTH_TOKEN_KEY_REFRESH_TOKEN, ONEKEY_OAUTH_STATE_KEY, } from '@onekeyhq/shared/src/consts/authConsts'; -import { ONEKEY_APP_DEEP_LINK } from '@onekeyhq/shared/src/consts/deeplinkConsts'; +import { + ONEKEY_APP_DEEP_LINK, + WalletConnectUniversalLinkFull, +} from '@onekeyhq/shared/src/consts/deeplinkConsts'; import { GoogleSignInConfigure, GoogleSignInConfigureIOS, @@ -56,29 +61,81 @@ export class OAuthPopup extends OAuthPopupBase { /** * Get OAuth redirect URL for native platforms. * - * Uses the deep link scheme: onekey-wallet://oauth_callback_native * Note: This is only used for WEB_BROWSER method. * GOOGLE_SIGNIN method doesn't need a redirect URL. + * + * IMPORTANT: Arbitrary HTTPS URLs (e.g., https://oauth-callback.onekey.so/...) + * will NOT work as callback URLs on native platforms. + * + * - iOS uses ASWebAuthenticationSession which only recognizes: + * 1. Custom URL Schemes (e.g., onekey-wallet://...) + * 2. Universal Links (requires apple-app-site-association on server) + * + * - Android uses Chrome Custom Tabs + Linking API which only catches: + * 1. Custom URL Schemes (e.g., onekey-wallet://...) + * 2. Android App Links (requires assetlinks.json on server) + * + * Using an arbitrary HTTPS URL will cause the browser to navigate to that URL + * instead of returning control to the app. The user would have to manually + * close the browser, resulting in a "cancel" error. */ static override getRedirectUrl(): Promise { - return Promise.resolve( - `${ONEKEY_APP_DEEP_LINK}${OAUTH_CALLBACK_NATIVE_PATH}`, - ); + // ❌ Does NOT work - arbitrary HTTPS URL, not recognized by native platforms + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const callbackUrl = `https://oauth-callback.onekey.so/${OAUTH_CALLBACK_NATIVE_PATH}`; + + // ✅ Works - Custom URL Scheme registered in app + const callbackUrlDeepLink = `${ONEKEY_APP_DEEP_LINK}${OAUTH_CALLBACK_NATIVE_PATH}`; + + // ✅ Works - Universal Link (if properly configured with apple-app-site-association) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const callbackUrlUniversalLink = `${WalletConnectUniversalLinkFull}${OAUTH_CALLBACK_NATIVE_PATH}`; + + return Promise.resolve(callbackUrlDeepLink); } /** * Open OAuth and return result. * - * Uses GOOGLE_SIGNIN method by default for native Google Sign-In experience. - * Falls back to WEB_BROWSER method if GoogleSignin is not available. + * Routes to the appropriate sign-in method based on provider and platform: + * - Apple on iOS: Uses native Apple Sign-In via expo-apple-authentication + * - Apple on Android: Uses WebBrowser method (Apple Sign-In via web OAuth) + * - Google: Uses @react-native-google-signin for native Google Sign-In + * - Fallback: Uses expo-web-browser for in-app browser OAuth */ static override async open( options: IOAuthPopupOptions, ): Promise { + const { provider } = options; const method = DEFAULT_NATIVE_OAUTH_METHOD; + if (method === ENativeOAuthMethod.WEB_BROWSER) { + // Use WebBrowser method + return OAuthPopup.openWithWebBrowser(options); + } - // Try GoogleSignin first (default) - if (method === ENativeOAuthMethod.GOOGLE_SIGNIN) { + // Apple Sign-In: Use native on iOS, WebBrowser on Android + if (provider === 'apple') { + if (platformEnv.isNativeIOS) { + try { + return await OAuthPopup.openWithAppleSignin(options); + } catch (error) { + // If Apple Sign-In fails due to setup issues, fall back to WebBrowser + if (OAuthPopup.shouldFallbackToWebBrowserForApple(error)) { + console.warn( + 'Apple Sign-In not available, falling back to WebBrowser:', + error instanceof Error ? error.message : error, + ); + return OAuthPopup.openWithWebBrowser(options); + } + throw error; + } + } + // Android: Use WebBrowser for Apple Sign-In + return OAuthPopup.openWithWebBrowser(options); + } + + // Google Sign-In: Try native GoogleSignin first (default) + if (provider === 'google') { try { return await OAuthPopup.openWithGoogleSignin(options); } catch (error) { @@ -94,16 +151,17 @@ export class OAuthPopup extends OAuthPopupBase { } } - // Use WebBrowser method - return OAuthPopup.openWithWebBrowser(options); + throw new OneKeyLocalError( + `Unsupported provider: ${provider || 'unknown'}`, + ); } - // ============ Private Methods - GoogleSignin ============ + // ============ Private Methods - Shared ============ /** * Generate a random nonce and its SHA-256 hash. - * The hash is passed to Google Sign-In, and the raw nonce to Supabase. - * This is required for iOS where the ID token contains a nonce. + * The hash is passed to OAuth provider, and the raw nonce to Supabase. + * This is required for ID token validation. */ private static async generateNonce(): Promise<{ rawNonce: string; @@ -117,6 +175,8 @@ export class OAuthPopup extends OAuthPopupBase { return { rawNonce, hashedNonce }; } + // ============ Private Methods - GoogleSignin ============ + /** * Check if error indicates GoogleSignin is not properly configured * and we should fall back to WebBrowser. @@ -136,6 +196,225 @@ export class OAuthPopup extends OAuthPopupBase { return false; } + // ============ Private Methods - Apple Sign-In ============ + + /** + * Check if error indicates Apple Sign-In is not properly configured + * and we should fall back to WebBrowser. + */ + private static shouldFallbackToWebBrowserForApple(error: unknown): boolean { + if (error instanceof Error) { + const message = error.message.toLowerCase(); + // Common Apple Sign-In setup errors that indicate fallback is needed + return ( + message.includes('not available') || + message.includes('not configured') || + message.includes('capability') || + message.includes('entitlement') + ); + } + return false; + } + + /** + * Open OAuth using expo-apple-authentication (iOS only). + * + * Flow: + * 1. Check if Apple Sign-In is available on this device + * 2. Call Apple Sign-In with requested scopes + * 3. Get Apple ID token from result + * 4. Exchange ID token for Supabase session using signInWithIdToken + * 5. Handle session persistence + * + * Reference: https://supabase.com/docs/guides/auth/social-login/auth-apple + */ + private static async openWithAppleSignin( + options: IOAuthPopupOptions, + ): Promise { + const { client, handleSessionPersistence } = options; + + if (!client) { + throw new OneKeyLocalError('Supabase client is required'); + } + + // Check if Apple Sign-In is available on this device + const isAvailable = await AppleAuthentication.isAvailableAsync(); + if (!isAvailable) { + throw new OneKeyLocalError( + 'Apple Sign-In is not available on this device. ' + + 'Make sure you are running iOS 13+ and have Sign in with Apple capability enabled.', + ); + } + + try { + // Generate nonce for security validation if enabled + // The hashed nonce is passed to Apple, raw nonce is passed to Supabase + // Reference: https://developer.apple.com/documentation/authenticationservices/asauthorizationopenidrequest/nonce + let rawNonce: string | undefined; + let hashedNonce: string | undefined; + + if (APPLE_SIGNIN_USE_NONCE) { + const nonceResult = await OAuthPopup.generateNonce(); + rawNonce = nonceResult.rawNonce; + hashedNonce = nonceResult.hashedNonce; + } + + // Perform Apple Sign-In with optional nonce + const credential = await OAuthPopup.performAppleSignIn(hashedNonce); + + // Get the identity token (JWT) from Apple + const idToken = credential.identityToken; + + if (!idToken) { + throw new OneKeyLocalError( + 'No identity token received from Apple Sign-In. ' + + 'Make sure Sign in with Apple is properly configured.', + ); + } + + // Exchange Apple ID token for Supabase session + // Reference: https://supabase.com/docs/guides/auth/social-login/auth-apple?platform=react-native + // The raw nonce is passed to Supabase to validate against the hashed nonce in the ID token + const { data, error } = await client.auth.signInWithIdToken({ + provider: 'apple', + token: idToken, + ...(rawNonce && { nonce: rawNonce }), + }); + + if (error) { + throw new OneKeyLocalError(error.message); + } + + if (!data.session) { + throw new OneKeyLocalError( + 'Failed to exchange Apple ID token for session', + ); + } + + const accessToken = data.session.access_token; + const refreshToken = data.session.refresh_token; + + // Handle session persistence + await handleSessionPersistence({ + accessToken, + refreshToken, + }); + + return { + success: true, + session: { accessToken, refreshToken }, + }; + } catch (error) { + // Handle specific Apple Sign-In errors + if (OAuthPopup.isAppleUserCancelledError(error)) { + throw new OneKeyLocalError('OAuth sign-in was cancelled'); + } + + throw error; + // Provide more helpful error messages for common Apple Sign-In issues + // const wrappedError = OAuthPopup.wrapAppleSignInError(error); + // throw wrappedError; + } + } + + /** + * Perform the actual Apple Sign-In request. + * Optionally uses nonce for replay attack protection when APPLE_SIGNIN_USE_NONCE is enabled. + * Reference: https://developer.apple.com/documentation/authenticationservices/asauthorizationopenidrequest/nonce + */ + private static async performAppleSignIn( + hashedNonce?: string, + ): Promise { + const credential = await AppleAuthentication.signInAsync({ + requestedScopes: [ + AppleAuthentication.AppleAuthenticationScope.FULL_NAME, + AppleAuthentication.AppleAuthenticationScope.EMAIL, + ], + ...(hashedNonce && { nonce: hashedNonce }), + }); + return credential; + } + + /** + * Check if error indicates user cancelled Apple Sign-In. + */ + private static isAppleUserCancelledError(error: unknown): boolean { + // expo-apple-authentication throws an error with code 'ERR_CANCELED' when user cancels + if (error instanceof Error) { + const errorWithCode = error as Error & { code?: string }; + if (errorWithCode.code === 'ERR_REQUEST_CANCELED') { + return true; + } + if (errorWithCode.code === 'ERR_CANCELED') { + return true; + } + // Also check message for cancellation indicators + const message = error.message.toLowerCase(); + return ( + message.includes('cancelled') || + message.includes('canceled') || + message.includes('user canceled') + ); + } + return false; + } + + /** + * Wrap Apple Sign-In errors with more helpful messages. + */ + private static wrapAppleSignInError(error: unknown): Error { + if (error instanceof OneKeyLocalError) { + return error; + } + + if (error instanceof Error) { + const errorWithCode = error as Error & { code?: string }; + const code = errorWithCode.code || ''; + const message = error.message.toLowerCase(); + + // Handle specific Apple error cases + // "The authorization attempt failed for an unknown reason" - generic Apple error + if ( + message.includes('authorization attempt failed') || + message.includes('unknown reason') + ) { + return new OneKeyLocalError( + 'Apple Sign-In failed. Please ensure:\n' + + '1. You are signed in to iCloud on this device\n' + + '2. Sign in with Apple is enabled in Settings > Apple ID > Sign-In & Security\n' + + '3. You are not running on a simulator without Apple ID configured', + ); + } + + // ERR_REQUEST_FAILED - generic request failure + if (code === 'ERR_REQUEST_FAILED') { + return new OneKeyLocalError( + 'Apple Sign-In request failed. Please check your network connection and try again.', + ); + } + + // ERR_INVALID_RESPONSE - invalid response from Apple + if (code === 'ERR_INVALID_RESPONSE') { + return new OneKeyLocalError( + 'Invalid response from Apple Sign-In. Please try again.', + ); + } + + // ERR_REQUEST_NOT_HANDLED - not properly configured + if (code === 'ERR_REQUEST_NOT_HANDLED') { + return new OneKeyLocalError( + 'Apple Sign-In is not properly configured. ' + + 'Please ensure Sign in with Apple capability is enabled.', + ); + } + + // Default: return the original error message + return new OneKeyLocalError(`Apple Sign-In failed: ${error.message}`); + } + + return new OneKeyLocalError('Apple Sign-In failed for an unknown reason'); + } + /** * Configure GoogleSignin with the provided options. * Returns the GoogleSignin instance for further use. @@ -319,6 +598,13 @@ export class OAuthPopup extends OAuthPopupBase { }, ); + Dialog.debugMessage({ + title: 'WebBrowser.openAuthSessionAsync', + debugMessage: { + browserResult, + }, + }); + if (browserResult.type === 'success' && browserResult.url) { const url = new URL(browserResult.url); @@ -355,22 +641,14 @@ export class OAuthPopup extends OAuthPopupBase { }; } - // Fallback: try to extract tokens directly from URL (implicit flow) - const { accessToken, refreshToken } = OAuthPopup.parseCallbackUrl( - browserResult.url, + // NOTE: We intentionally do NOT support Implicit Flow fallback here. + // Implicit Flow returns access_token directly in URL hash, which is less secure. + // Since Supabase is configured with flowType: 'pkce', only authorization codes + // are returned, and this fallback would never be triggered anyway. + // If we reach here without a code, the OAuth flow has failed. + throw new OneKeyLocalError( + 'OAuth callback missing authorization code. PKCE flow required.', ); - - if (accessToken && refreshToken) { - await handleSessionPersistence({ - accessToken, - refreshToken, - }); - - return { - success: true, - session: { accessToken, refreshToken }, - }; - } } if (browserResult.type === 'cancel') { @@ -379,22 +657,4 @@ export class OAuthPopup extends OAuthPopupBase { throw new OneKeyLocalError('OAuth sign-in failed'); } - - /** - * Parse tokens from callback URL (for WebBrowser method). - */ - private static parseCallbackUrl(url: string): { - accessToken: string | null; - refreshToken: string | null; - } { - const parsedUrl = new URL(url); - const hashParams = new URLSearchParams( - parsedUrl.hash.substring(1) || parsedUrl.search.substring(1), - ); - - return { - accessToken: hashParams.get(OAUTH_TOKEN_KEY_ACCESS_TOKEN), - refreshToken: hashParams.get(OAUTH_TOKEN_KEY_REFRESH_TOKEN), - }; - } } diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/types.ts b/packages/kit/src/components/OneKeyAuth/OAuthPopup/types.ts index 43fc2d5864aa..e05748082251 100644 --- a/packages/kit/src/components/OneKeyAuth/OAuthPopup/types.ts +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/types.ts @@ -35,6 +35,8 @@ export type IOpenOAuthPopupOptions = { * Unified OAuth popup options for all platforms */ export interface IOAuthPopupOptions { + // The OAuth provider (google, apple, etc.) + provider?: 'google' | 'apple'; // The OAuth authorization URL to open authUrl?: string; // The OAuth redirect URL (with onekey_oauth_state if needed) diff --git a/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx b/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx index b5b3e5f0e901..456f7ab0b68f 100644 --- a/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx +++ b/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx @@ -3,6 +3,7 @@ import { useCallback, useMemo } from 'react'; import { useIntl } from 'react-intl'; +import { Dialog } from '@onekeyhq/components'; import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; import { ETranslations } from '@onekeyhq/shared/src/locale'; @@ -107,9 +108,36 @@ export function useSupabaseAuth() { if (!authUrl) { throw new OneKeyLocalError('Failed to get OAuth URL'); } + /* + iOS: + { + "authUrl": "https://wtspqckturkzhstyjabx.supabase.co/auth/v1/authorize?provider=apple&redirect_to=https%3A%2F%2Foauth-callback.onekey.so%2Foauth_callback_native%3Fonekey_oauth_state%3D3af5c82abbfb19da14a00f6035828bdf&code_challenge=xxxx&code_challenge_method=plain&prompt=select_account", + "provider": "apple", + "redirectTo": "https://oauth-callback.onekey.so/oauth_callback_native?onekey_oauth_state=3af5c82abbfb19da14a00f6035828bdf" + } + https://oauth-callback.onekey.so/oauth_callback_native?code=xxxx&onekey_oauth_state=3af5c82abbfb19da14a00f6035828bdf + + Desktop: + { + "authUrl": "https://wtspqckturkzhstyjabx.supabase.co/auth/v1/authorize?provider=apple&redirect_to=http%3A%2F%2F127.0.0.1%3A62416%2Foauth_callback_desktop%3Fonekey_oauth_state%3D2fd6480e3004ad6aef7d6a72dc37455b&code_challenge=xxxx&code_challenge_method=s256&prompt=select_account", + "provider": "apple", + "redirectTo": "http://127.0.0.1:62416/oauth_callback_desktop?onekey_oauth_state=2fd6480e3004ad6aef7d6a72dc37455b" + } + http://127.0.0.1:62416/oauth_callback_desktop?code=xxxx&onekey_oauth_state=2fd6480e3004ad6aef7d6a72dc37455b + */ + + Dialog.debugMessage({ + title: 'performOAuthSignIn', + debugMessage: { + provider, + redirectTo, + authUrl, + }, + }); // Open OAuth popup using platform-specific implementation return OAuthPopup.open({ + provider, authUrl, redirectTo, client: clientTemp, diff --git a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/CryptoGallery.tsx b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/CryptoGallery.tsx index 04b4500159b4..6c22fce639fd 100644 --- a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/CryptoGallery.tsx +++ b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/CryptoGallery.tsx @@ -231,6 +231,154 @@ function AESCbcTest() { ); } +/** + * Test crypto.subtle polyfill + * This polyfill is required for Supabase Auth PKCE flow on React Native + * @see packages/shared/src/appCrypto/cryptoSubtlePolyfill.js + */ +function CryptoSubtlePolyfillTest() { + const [result, setResult] = useState(''); + + const testCryptoSubtle = async () => { + try { + const tasks: IRunAppCryptoTestTaskResult[] = []; + + // Test 1: Check if crypto.subtle exists + tasks.push( + await runAppCryptoTestTask({ + expect: true, + name: 'crypto.subtle exists', + fn: async () => { + return ( + typeof crypto !== 'undefined' && + typeof crypto.subtle !== 'undefined' && + typeof crypto.subtle.digest === 'function' + ); + }, + }), + ); + + // Test 2: SHA-256 digest test + // Hash of "hello" in SHA-256 should be: + // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 + const testData = new TextEncoder().encode('hello'); + const expectedSha256 = + '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'; + + tasks.push( + await runAppCryptoTestTask({ + expect: expectedSha256, + name: 'crypto.subtle.digest(SHA-256)', + fn: async () => { + const hashBuffer = await crypto.subtle.digest('SHA-256', testData); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + return hashHex; + }, + }), + ); + + // Test 3: SHA-512 digest test + // Hash of "hello" in SHA-512 + const expectedSha512 = + '9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043'; + + tasks.push( + await runAppCryptoTestTask({ + expect: expectedSha512, + name: 'crypto.subtle.digest(SHA-512)', + fn: async () => { + const hashBuffer = await crypto.subtle.digest('SHA-512', testData); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + return hashHex; + }, + }), + ); + + // Test 4: SHA-1 digest test + // Hash of "hello" in SHA-1 + const expectedSha1 = 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d'; + + tasks.push( + await runAppCryptoTestTask({ + expect: expectedSha1, + name: 'crypto.subtle.digest(SHA-1)', + fn: async () => { + const hashBuffer = await crypto.subtle.digest('SHA-1', testData); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + return hashHex; + }, + }), + ); + + // Test 5: Supabase PKCE simulation test + // Simulate the actual PKCE flow that Supabase uses + const codeVerifier = 'test-code-verifier-for-pkce-flow'; + const verifierData = new TextEncoder().encode(codeVerifier); + + tasks.push( + await runAppCryptoTestTask({ + expect: + 'e23dacc73c1e1e4acc2da94fff24f54ae01f1cb2b94a77ebf5ea27e22b03e614', + name: 'crypto.subtle.digest(PKCE simulation)', + fn: async () => { + const hashBuffer = await crypto.subtle.digest( + 'SHA-256', + verifierData, + ); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + return hashHex; + }, + }), + ); + + setResult( + stringUtils.stableStringify( + tasks, + stringUtils.STRINGIFY_REPLACER.bufferToHex, + 2, + ), + ); + + const allPassed = tasks.every((t) => t.passed); + if (allPassed) { + Toast.success({ + title: 'crypto.subtle polyfill test passed', + }); + } else { + Toast.error({ + title: 'crypto.subtle polyfill test failed', + }); + } + } catch (error) { + setResult(`Error: ${(error as Error).message}`); + Toast.error({ + title: `crypto.subtle test failed: ${(error as Error).message}`, + }); + } + }; + + return ( + + + {result ? {result} : null} + + ); +} + function SecretFunctionsTest() { const [result, setResult] = useState(''); @@ -731,6 +879,9 @@ const CryptoGallery = () => ( + + + diff --git a/packages/shared/src/appCrypto/cryptoSubtlePolyfill.js b/packages/shared/src/appCrypto/cryptoSubtlePolyfill.js new file mode 100644 index 000000000000..9fe4b150f310 --- /dev/null +++ b/packages/shared/src/appCrypto/cryptoSubtlePolyfill.js @@ -0,0 +1,100 @@ +const platformEnv = require('@onekeyhq/shared/src/platformEnv'); + +/** + * Polyfill crypto.subtle for React Native + * + * Purpose: + * - Enables Supabase Auth PKCE to use s256 (SHA-256) instead of plain method + * - Supabase's @supabase/auth-js GoTrueClient checks crypto.subtle.digest + * to determine PKCE code_challenge method support + * + * Implementation Notes: + * - Uses react-native-aes-crypto native module for SHA hashing + * - The native module (both iOS/Android) expects HEX-ENCODED string input: + * - iOS: AesCrypt.m uses [self fromHex:input] to decode hex to bytes + * - Android: Aes.java uses Hex.decode(data) to decode hex to bytes + * - This is why we convert ArrayBuffer -> hex string before calling native functions + * + * Prerequisites: + * - Must be loaded AFTER the crypto polyfill in polyfillsPlatform.js + * - Requires react-native-aes-crypto native module to be linked + * + * Limitations: + * - Only implements digest() method (minimum required for Supabase PKCE) + * - Supports SHA-1, SHA-256, SHA-512 algorithms only + */ +if (platformEnv.isNative) { + if (typeof crypto !== 'undefined' && typeof crypto.subtle === 'undefined') { + // Lazy load RN_AES to avoid circular dependency issues at module initialization + let RN_AES = null; + const getRNAES = () => { + if (!RN_AES) { + RN_AES = require('react-native-aes-crypto').default; + } + return RN_AES; + }; + + /** + * Convert hex string to ArrayBuffer + * Used to convert native hash result (hex) back to Web Crypto API format (ArrayBuffer) + */ + const hexToArrayBuffer = (hex) => { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); + } + return bytes.buffer; + }; + + /** + * Convert ArrayBuffer/Uint8Array to hex string + * Required because react-native-aes-crypto native module expects hex-encoded input + * @see iOS: AesCrypt.m - [self fromHex:input] + * @see Android: Aes.java - Hex.decode(data) + */ + const arrayBufferToHex = (buffer) => { + const bytes = new Uint8Array(buffer); + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + }; + + // Create a minimal crypto.subtle polyfill (only digest() for Supabase PKCE) + crypto.subtle = { + /** + * Compute hash digest using react-native-aes-crypto native module + * Implements Web Crypto API's SubtleCrypto.digest() interface + * @param {string} algorithm - Hash algorithm (e.g., 'SHA-256', 'SHA-512', 'SHA-1') + * @param {ArrayBuffer|Uint8Array} data - Data to hash + * @returns {Promise} - Hash result as ArrayBuffer + */ + digest: async (algorithm, data) => { + // Normalize algorithm name: 'SHA-256' -> 'SHA256' + const normalizedAlgorithm = algorithm.toUpperCase().replace('-', ''); + // Convert input to hex (required by react-native-aes-crypto native module) + const hexData = arrayBufferToHex(data); + const rnAes = getRNAES(); + + let hashHex; + switch (normalizedAlgorithm) { + case 'SHA256': + hashHex = await rnAes.sha256(hexData); + break; + case 'SHA512': + hashHex = await rnAes.sha512(hexData); + break; + case 'SHA1': + hashHex = await rnAes.sha1(hexData); + break; + default: + throw new Error( + `crypto.subtle.digest: Unsupported algorithm "${algorithm}"`, + ); + } + + // Convert hex result back to ArrayBuffer (Web Crypto API format) + return hexToArrayBuffer(hashHex); + }, + }; + } +} diff --git a/packages/shared/src/consts/authConsts.ts b/packages/shared/src/consts/authConsts.ts index a90d48d5ce3c..ab0c0c38ecb3 100644 --- a/packages/shared/src/consts/authConsts.ts +++ b/packages/shared/src/consts/authConsts.ts @@ -65,10 +65,8 @@ export enum EExtensionOAuthMethod { } export enum ENativeOAuthMethod { - // ✅ RECOMMENDED: Use @react-native-google-signin/google-signin with signInWithIdToken - // Uses native Google Sign-In UI for better UX - // Gets Google ID token and exchanges it for Supabase session - GOOGLE_SIGNIN = 'GOOGLE_SIGNIN', + // ✅ RECOMMENDED: Use @react-native-google-signin/google-signin or expo-apple-authentication with signInWithIdToken + NATIVE_SDK = 'NATIVE_SDK', // Fallback: Use expo-web-browser.openAuthSessionAsync // Opens in-app browser for OAuth, uses deep link callback @@ -115,6 +113,11 @@ export const GOOGLE_OAUTH_DEFAULT_SCOPES = [ export const EXTENSION_OAUTH_USE_PKCE_FLOW = true; +// Apple Sign-In nonce support +// When enabled, a nonce will be generated and passed to Apple Sign-In for replay attack protection +// Reference: https://developer.apple.com/documentation/authenticationservices/asauthorizationopenidrequest/nonce +export const APPLE_SIGNIN_USE_NONCE = true; + // Email OTP export const EMAIL_OTP_COUNTDOWN_SECONDS = 60; @@ -124,7 +127,7 @@ export const DEFAULT_EXTENSION_OAUTH_METHOD: EExtensionOAuthMethod = export const DEFAULT_DESKTOP_OAUTH_METHOD: EDesktopOAuthMethod = EDesktopOAuthMethod.LOCALHOST_SERVER; export const DEFAULT_NATIVE_OAUTH_METHOD: ENativeOAuthMethod = - ENativeOAuthMethod.GOOGLE_SIGNIN; + ENativeOAuthMethod.WEB_BROWSER; // Google OAuth clients // - https://console.cloud.google.com/auth/clients diff --git a/packages/shared/src/polyfills/polyfillsPlatform.js b/packages/shared/src/polyfills/polyfillsPlatform.js index f2d100e221fa..ba950eba1850 100644 --- a/packages/shared/src/polyfills/polyfillsPlatform.js +++ b/packages/shared/src/polyfills/polyfillsPlatform.js @@ -290,4 +290,13 @@ if (platformEnv.isNative) { } } +// Polyfill crypto.subtle for React Native +// This must be loaded AFTER the crypto polyfill (line 145-154) because it extends the crypto object. +// Purpose: Enable Supabase Auth PKCE flow to use SHA-256 code_challenge (s256 method) +// instead of falling back to plain method when crypto.subtle is unavailable. +// @see @supabase/auth-js GoTrueClient.ts - checks crypto.subtle.digest for PKCE support +if (platformEnv.isNative) { + require('@onekeyhq/shared/src/appCrypto/cryptoSubtlePolyfill'); +} + console.log('polyfillsPlatform.native shim loaded'); From 385a00bf01aa65b8c277593ac51b1b42a6d5b16f Mon Sep 17 00:00:00 2001 From: morizon Date: Wed, 24 Dec 2025 20:21:04 +0800 Subject: [PATCH 17/66] fix(crypto): update crypto.subtle.digest test result --- .../Gallery/Components/stories/CryptoGallery.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/CryptoGallery.tsx b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/CryptoGallery.tsx index 6c22fce639fd..8f929944edd7 100644 --- a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/CryptoGallery.tsx +++ b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/CryptoGallery.tsx @@ -246,13 +246,13 @@ function CryptoSubtlePolyfillTest() { // Test 1: Check if crypto.subtle exists tasks.push( await runAppCryptoTestTask({ - expect: true, + expect: 'true', name: 'crypto.subtle exists', fn: async () => { - return ( + return String( typeof crypto !== 'undefined' && - typeof crypto.subtle !== 'undefined' && - typeof crypto.subtle.digest === 'function' + typeof crypto.subtle !== 'undefined' && + typeof crypto.subtle.digest === 'function', ); }, }), @@ -327,7 +327,7 @@ function CryptoSubtlePolyfillTest() { tasks.push( await runAppCryptoTestTask({ expect: - 'e23dacc73c1e1e4acc2da94fff24f54ae01f1cb2b94a77ebf5ea27e22b03e614', + 'f0c2f8b2aad90ad913c0561953b38bf3d435f59b5e4ef24eebc6605b0b444907', name: 'crypto.subtle.digest(PKCE simulation)', fn: async () => { const hashBuffer = await crypto.subtle.digest( @@ -351,7 +351,9 @@ function CryptoSubtlePolyfillTest() { ), ); - const allPassed = tasks.every((t) => t.passed); + const allPassed = tasks.every( + (t) => t.isCorrect === AppCryptoTestEmoji.isCorrect, + ); if (allPassed) { Toast.success({ title: 'crypto.subtle polyfill test passed', From 06f3b80070f64ccabef4466151b8ebb4f8aeab14 Mon Sep 17 00:00:00 2001 From: morizon Date: Wed, 24 Dec 2025 20:30:58 +0800 Subject: [PATCH 18/66] fix: lint --- .../OAuthPopup/APPLE_SIGNIN_SETUP.md | 461 ------------------ .../OAuthPopup/GOOGLE_SIGNIN_SETUP.md | 236 --------- 2 files changed, 697 deletions(-) delete mode 100644 packages/kit/src/components/OneKeyAuth/OAuthPopup/APPLE_SIGNIN_SETUP.md delete mode 100644 packages/kit/src/components/OneKeyAuth/OAuthPopup/GOOGLE_SIGNIN_SETUP.md diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/APPLE_SIGNIN_SETUP.md b/packages/kit/src/components/OneKeyAuth/OAuthPopup/APPLE_SIGNIN_SETUP.md deleted file mode 100644 index 890daeca4e9f..000000000000 --- a/packages/kit/src/components/OneKeyAuth/OAuthPopup/APPLE_SIGNIN_SETUP.md +++ /dev/null @@ -1,461 +0,0 @@ -# Apple Sign-In Setup Guide - -This document describes how to configure Apple Sign-In for the OneKey app, with a focus on web platform setup. - -## Overview - -OneKey uses Apple Sign-In with Supabase for authentication. The web platform uses Supabase's OAuth flow: - -1. User clicks "Sign in with Apple" -2. Supabase generates OAuth URL with PKCE flow -3. User authenticates with Apple in popup window -4. Apple redirects to Supabase with authorization code -5. Supabase exchanges code for session -6. OneKey receives access/refresh tokens - -## Prerequisites - -Before starting, ensure you have: - -1. **Apple Developer Program Membership** ($99/年) - https://developer.apple.com/programs/enroll/ - > ⚠️ **免费开发者账户不支持 Sign in with Apple!** 你需要付费会员才能: - > - 创建 App ID 和 Services ID - > - 生成私钥 (.p8 文件) - > - 配置 OAuth 回调 URL - -2. Access to **Supabase Dashboard** (https://supabase.com/dashboard) - -## Local Development Testing - -**可以在本地测试 Apple Sign-In!** 原理如下: - -``` -┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ ┌─────────────┐ -│ localhost │────>│ Supabase (HTTPS)│────>│ Apple │────>│ Supabase │ -│ :3000 │ │ OAuth URL │ │ Sign-In │ │ Callback │ -└─────────────┘ └──────────────────┘ └─────────────┘ └─────────────┘ - │ - ▼ - ┌─────────────────┐ - │ localhost:3000 │ - │ /oauth_callback│ - └─────────────────┘ -``` - -**关键点**: -- Apple OAuth 的 callback 先到达 Supabase(HTTPS),不是直接到 localhost -- Supabase 再重定向到你的 localhost -- 所以 **localhost 不需要 HTTPS** - -### 本地开发配置步骤 - -1. **Apple Developer Console - Services ID 配置**: - - **Domains**: 添加 `localhost` - - **Return URLs**: 保持 Supabase callback URL(不需要改) - ``` - https://zvxscjkvkjepbrjncvzt.supabase.co/auth/v1/callback - ``` - -2. **Supabase Dashboard - Redirect URLs**: - - 添加本地开发 URL: - ``` - http://localhost:3000/oauth_callback_web/ - http://127.0.0.1:3000/oauth_callback_web/ - ``` - -3. **启动本地开发服务器**: - ```bash - yarn app:web - ``` - -4. **访问** `http://localhost:3000` 并测试 Apple 登录 - -> **注意**: 如果端口不是 3000,请相应修改 Supabase 的 Redirect URLs - ---- - -## Part 1: Apple Developer Console Configuration - -### Step 1: Create an App ID - -1. Go to [Apple Developer Console](https://developer.apple.com/account) -2. Navigate to **Certificates, Identifiers & Profiles** → **Identifiers** -3. Click the **+** button to create a new identifier -4. Select **App IDs** → **Continue** -5. Select **App** type → **Continue** -6. Fill in the details: - - **Description**: OneKey Wallet - - **Bundle ID**: `so.onekey.wallet.desktop` (or your bundle ID) -7. Under **Capabilities**, check **Sign in with Apple** -8. Click **Continue** → **Register** - -### Step 2: Create a Services ID (Required for Web) - -1. In **Identifiers**, click **+** again -2. Select **Services IDs** → **Continue** -3. Fill in the details: - - **Description**: OneKey Web Login - - **Identifier**: `so.onekey.wallet.web` (this will be your `client_id`) -4. Click **Continue** → **Register** -5. **Click on the newly created Services ID** to configure it -6. Check **Sign in with Apple** → Click **Configure** -7. Configure the Web Authentication: - - **Primary App ID**: Select your App ID from Step 1 - - **Domains and Subdomains**: Add your domains, e.g.: - ``` - app.onekey.so - localhost - ``` - - **Return URLs** (Redirect URIs): Add: - ``` - https://zvxscjkvkjepbrjncvzt.supabase.co/auth/v1/callback - ``` - > **Note**: Replace with your Supabase project URL. Find it at: - > Supabase Dashboard → Project Settings → API → Project URL -8. Click **Save** → **Continue** → **Save** - -### Step 3: Create a Private Key - -1. Navigate to **Keys** in the left sidebar -2. Click **+** to create a new key -3. Fill in: - - **Key Name**: OneKey Sign In Key -4. Check **Sign in with Apple** → Click **Configure** -5. Select your **Primary App ID** from Step 1 -6. Click **Save** → **Continue** → **Register** -7. **Download the key file** (`.p8` file) - you can only download this once! -8. Note down the **Key ID** (10-character string) - -### Step 4: Note Your Team ID - -1. Go to [Apple Developer Account](https://developer.apple.com/account) -2. Your **Team ID** is shown in the top right, or under **Membership Details** - ---- - -## Part 2: Supabase Configuration - -### Step 1: Configure Apple Provider - -1. Go to [Supabase Dashboard](https://supabase.com/dashboard) -2. Select your project -3. Navigate to **Authentication** → **Providers** -4. Find **Apple** and click to expand -5. Toggle **Enable Sign in with Apple** -6. Fill in the configuration: - -| Field | Value | Description | -|-------|-------|-------------| -| **Client ID (Services ID)** | `so.onekey.wallet.web` | The Services ID identifier from Step 2 | -| **Secret Key** | `-----BEGIN PRIVATE KEY-----...` | Contents of the `.p8` file downloaded in Step 3 | -| **Key ID** | `ABC1234567` | The 10-character Key ID from Step 3 | -| **Team ID** | `TEAM123456` | Your 10-character Team ID from Step 4 | - -7. Click **Save** - -### Step 2: Configure Redirect URLs - -1. Navigate to **Authentication** → **URL Configuration** -2. Add your application's OAuth callback URLs to **Redirect URLs**: - -For Web Platform: -``` -https://app.onekey.so/oauth_callback_web/ -http://localhost:3000/oauth_callback_web/ -``` - -For Desktop Platform (localhost server): -``` -http://localhost:3846/oauth_callback_desktop -http://127.0.0.1:3846/oauth_callback_desktop -``` - -> **Note**: The trailing slash matters! Match exactly what your application sends. - -### Step 3: Verify Configuration - -Test the setup: -1. Go to **Authentication** → **Providers** → **Apple** -2. Check that all fields are filled correctly -3. Ensure no error messages are displayed - ---- - -## Part 3: Web Platform Implementation - -The web platform OAuth is already implemented in the codebase. Here's how it works: - -### OAuth Flow - -``` -┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ -│ OneKey │────>│ Supabase │────>│ Apple │────>│ Callback │ -│ Web App │ │ OAuth URL │ │ Sign-In │ │ Handler │ -└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘ - │ │ - │ │ - └──────────────────── Session Tokens ──────────────────────────┘ -``` - -### Key Files - -| File | Purpose | -|------|---------| -| `OAuthPopup.tsx` | Web popup OAuth implementation | -| `OAuthPopupBase.ts` | Shared OAuth utilities | -| `useSupabaseAuth.tsx` | React hook for OAuth sign-in | -| `authConsts.ts` | OAuth configuration constants | - -### Usage in Code - -```typescript -import { useSupabaseAuth } from '@onekeyhq/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth'; - -function LoginComponent() { - const { signInWithApple } = useSupabaseAuth(); - - const handleAppleLogin = async () => { - try { - const result = await signInWithApple({ - persistSession: true, // Save session to storage - }); - - if (result.success) { - console.log('Logged in with Apple!'); - console.log('Access Token:', result.session?.accessToken); - } - } catch (error) { - console.error('Apple Sign-In failed:', error); - } - }; - - return ( - - ); -} -``` - -### Redirect URL Configuration - -The redirect URL is defined in `authConsts.ts`: - -```typescript -export const OAUTH_CALLBACK_WEB_PATH = '/oauth_callback_web/'; -``` - -The full redirect URL is constructed as: -``` -${window.location.origin}/oauth_callback_web/ -// Example: https://app.onekey.so/oauth_callback_web/ -``` - ---- - -## Part 4: Desktop & Extension Configuration - -### Desktop (Electron) - -Desktop uses a localhost HTTP server for OAuth callback: - -1. The redirect URL is: `http://localhost:3846/oauth_callback_desktop` -2. This is already configured in `OAuthPopup.desktop.tsx` -3. Make sure to add this URL in: - - Apple Developer Console → Services ID → Return URLs - - Supabase Dashboard → URL Configuration → Redirect URLs - -### Browser Extension - -Extensions use `chrome.identity.launchWebAuthFlow`: - -1. The redirect URL format is: `https://.chromiumapp.org` -2. This is automatically handled by Chrome -3. Add the redirect URL in: - - Apple Developer Console → Services ID → Return URLs - - Supabase Dashboard → URL Configuration → Redirect URLs - -> **Note**: You need to know your extension ID. For development, use a stable extension ID by configuring `key` in `manifest.json`. - ---- - -## Part 5: iOS & Android Configuration - -### iOS Native - -For iOS, you can use native Apple Sign-In: - -1. Enable "Sign in with Apple" capability in Xcode: - - Select your target → **Signing & Capabilities** - - Click **+ Capability** → **Sign in with Apple** - -2. The bundle ID must match your App ID from Apple Developer Console - -3. Native sign-in returns an ID token that can be exchanged with Supabase: - -```typescript -// Example using @invertase/react-native-apple-authentication -import appleAuth from '@invertase/react-native-apple-authentication'; - -const appleAuthResult = await appleAuth.performRequest({ - requestedOperation: appleAuth.Operation.LOGIN, - requestedScopes: [appleAuth.Scope.EMAIL, appleAuth.Scope.FULL_NAME], -}); - -// Exchange Apple ID token with Supabase -const { data, error } = await supabase.auth.signInWithIdToken({ - provider: 'apple', - token: appleAuthResult.identityToken, -}); -``` - -### Android - 使用 Web OAuth 流程 - -**Android 可以使用 Apple 登录!** Apple 官方支持通过 Web OAuth 流程在 Android 上实现 Sign in with Apple。 - -参考: [Apple 官方文档 - 在网站和其他平台上使用 Sign in with Apple](https://developer.apple.com/cn/sign-in-with-apple/usage-guidelines-for-websites-and-other-platforms/) - -#### 实现原理 - -Android 没有原生的 Apple Sign-In SDK,但可以通过 **expo-web-browser** 打开 Web OAuth 流程: - -``` -┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ -│ Android │────>│ In-App │────>│ Apple │────>│ Deep Link │ -│ App │ │ Browser │ │ Sign-In │ │ Callback │ -└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘ -``` - -#### 当前实现 - -在 `OAuthPopup.native.tsx` 中: -- `signInWithApple` 会自动使用 `openWithWebBrowser` 方法 -- 使用 `expo-web-browser.openAuthSessionAsync` 打开 Apple 登录页面 -- 回调通过 deep link `onekey-wallet://oauth_callback_native` 返回 App - -#### 配置要求 - -1. **Apple Developer Console**: - - Services ID 的 **Return URLs** 需要添加 Supabase callback: - ``` - https://zvxscjkvkjepbrjncvzt.supabase.co/auth/v1/callback - ``` - -2. **Supabase Dashboard**: - - **Redirect URLs** 需要添加 deep link: - ``` - onekey-wallet://oauth_callback_native - ``` - -3. **Android App**: - - 确保 `onekey-wallet://` deep link scheme 已正确配置 - - 在 `AndroidManifest.xml` 中配置 intent filter - -#### 代码使用 - -```typescript -// Android 上调用 signInWithApple 会自动使用 WebBrowser 流程 -const { signInWithApple } = useSupabaseAuth(); - -const result = await signInWithApple({ persistSession: true }); -``` - -#### 注意事项 - -| 对比项 | iOS | Android | -|-------|-----|---------| -| 登录方式 | 可用原生 SDK 或 Web 流程 | 只能用 Web 流程 | -| 用户体验 | 原生弹窗,体验更好 | 打开内置浏览器 | -| Face ID / 指纹 | 支持 | 需要输入 Apple ID 密码 | -| 配置复杂度 | 需要 Xcode 配置 | 只需 Supabase 配置 | - ---- - -## Troubleshooting - -### "Invalid client_id" - -**Cause**: The Services ID doesn't match or isn't configured correctly. - -**Solution**: -1. Verify the Services ID in Supabase matches exactly (case-sensitive) -2. Ensure the Services ID has "Sign in with Apple" enabled -3. Check the domain/return URL configuration - -### "redirect_uri_mismatch" - -**Cause**: The redirect URL doesn't match Apple's configuration. - -**Solution**: -1. Check the Return URL in Apple Developer Console -2. Ensure the URL in Supabase matches exactly (including trailing slash) -3. Verify the domain is listed in the Services ID configuration - -### "invalid_grant" or Token Exchange Fails - -**Cause**: The private key or team configuration is incorrect. - -**Solution**: -1. Re-download the `.p8` private key -2. Verify the Key ID matches (10 characters) -3. Verify the Team ID matches (10 characters) -4. Ensure the private key content includes the full `-----BEGIN PRIVATE KEY-----` header - -### Popup Blocked - -**Cause**: Browser blocking popup window. - -**Solution**: -1. User needs to allow popups for the site -2. Ensure the popup is triggered by user action (click handler) - -### "User cancelled" Error - -**Cause**: User closed the Apple Sign-In popup without completing authentication. - -**Solution**: This is expected behavior - handle gracefully in UI. - ---- - -## Security Considerations - -1. **Never expose the private key** (`.p8` file) in client-side code -2. **Use HTTPS** for all production redirect URLs -3. **Validate state parameters** to prevent CSRF attacks (already implemented) -4. **Use PKCE flow** for enhanced security (already enabled) - ---- - -## Quick Reference: Required URLs - -### Apple Developer Console - Services ID Configuration - -**Domains**: -``` -app.onekey.so -localhost (for development) -``` - -**Return URLs**: -``` -https://zvxscjkvkjepbrjncvzt.supabase.co/auth/v1/callback -``` - -### Supabase - Redirect URLs - -``` -https://app.onekey.so/oauth_callback_web/ -http://localhost:3000/oauth_callback_web/ -http://localhost:3846/oauth_callback_desktop -http://127.0.0.1:3846/oauth_callback_desktop -https://.chromiumapp.org -``` - ---- - -## Related Documentation - -- [Supabase Apple OAuth Guide](https://supabase.com/docs/guides/auth/social-login/auth-apple) -- [Apple Sign in with Apple Documentation](https://developer.apple.com/sign-in-with-apple/) -- [Apple REST API for Sign in with Apple](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api) - diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/GOOGLE_SIGNIN_SETUP.md b/packages/kit/src/components/OneKeyAuth/OAuthPopup/GOOGLE_SIGNIN_SETUP.md deleted file mode 100644 index 6e11d9f7f831..000000000000 --- a/packages/kit/src/components/OneKeyAuth/OAuthPopup/GOOGLE_SIGNIN_SETUP.md +++ /dev/null @@ -1,236 +0,0 @@ -# Google Sign-In Setup Guide - -This document describes how to configure Google Sign-In for each platform in the OneKey app. - -## Overview - -OneKey uses Google Sign-In with Supabase for authentication. The flow is: - -1. User signs in with Google (platform-specific method) -2. Get Google ID token -3. Exchange ID token for Supabase session via `signInWithIdToken` - -## Google Cloud Console Configuration - -### Prerequisites - -1. Go to [Google Cloud Console](https://console.cloud.google.com/) -2. Select or create a project -3. Enable **Google Sign-In API** (APIs & Services → Library → Google Sign-In API) - -### OAuth Client IDs - -Create OAuth 2.0 Client IDs at: **APIs & Services → Credentials → Create Credentials → OAuth Client ID** - -| Platform | Type | Client ID | -|----------|------|-----------| -| Web | Web application | `244450898872-d22ubafv8ca38s6fp0kflhdr6e3s386u.apps.googleusercontent.com` | -| iOS | iOS | `244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt.apps.googleusercontent.com` | -| Android | Web application | Use Web Client ID (required for `idToken`) | -| Extension | Web application | Use Web Client ID | - -> **Important**: For Android/iOS native apps, you need a **Web Client ID** to get `idToken` for Supabase. The native client ID alone won't work with `signInWithIdToken`. - ---- - -## iOS Configuration - -### 1. Install Dependencies - -Ensure `@react-native-google-signin/google-signin` is NOT excluded in `apps/mobile/package.json`: - -```json -{ - "excludePackagesFromPodInstall": { - "exclude": [] // Remove @react-native-google-signin/google-signin from exclude list - } -} -``` - -### 2. Run Pod Install - -```bash -cd apps/mobile/ios -pod install -``` - -### 3. Configure URL Scheme in Info.plist (Required) - -Add the reversed client ID as a URL scheme in `apps/mobile/ios/OneKeyWallet/Info.plist`: - -```xml -CFBundleURLTypes - - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - - com.googleusercontent.apps.244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt - - - -``` - -> **Note**: The URL scheme is the iOS Client ID reversed. For `244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt.apps.googleusercontent.com`, use `com.googleusercontent.apps.244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt`. - -### 4. GoogleService-Info.plist (Optional) - -`GoogleService-Info.plist` is **NOT required** for Google Sign-In alone. It's only needed if you use other Firebase services (Analytics, Crashlytics, etc.). - -If you do need it: -1. Go to [Firebase Console](https://console.firebase.google.com/) -2. Create a project or select existing one -3. Add an iOS app with your bundle ID -4. Download `GoogleService-Info.plist` -5. Add to Xcode project - -### 5. Verify Configuration - -The iOS Client ID in `packages/shared/src/consts/authConsts.ts` should match: - -```typescript -export const GOOGLE_OAUTH_CLIENT_IDS = { - IOS: '244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt.apps.googleusercontent.com', -}; -``` - -And in `packages/shared/src/consts/googleSignConsts.ts`: - -```typescript -export const GoogleSignInConfigureIOS = { - scopes: ['openid', 'profile', 'email'], - offlineAccess: false, - iosClientId: GOOGLE_OAUTH_CLIENT_IDS.IOS, -}; -``` - -### 6. Rebuild iOS App - -```bash -# Clean and rebuild -cd apps/mobile/ios -rm -rf build Pods -pod install -cd .. -yarn ios -``` - ---- - -## Android Configuration - -### 1. Install Dependencies - -Ensure `@react-native-google-signin/google-signin` is properly linked (automatic with autolinking). - -### 2. Add google-services.json - -1. Download `google-services.json` from Firebase Console -2. Place it in `apps/mobile/android/app/google-services.json` - -### 3. Configure Signing (Important!) - -Google Sign-In requires proper app signing. In development: - -1. Generate a debug keystore SHA-1: - ```bash - cd apps/mobile/android - ./gradlew signingReport - ``` - -2. Add the SHA-1 fingerprint to your Google Cloud OAuth client: - - Go to Google Cloud Console → APIs & Services → Credentials - - Edit your Android OAuth Client - - Add the SHA-1 fingerprint - -### 4. Verify Configuration - -The Web Client ID in `packages/shared/src/consts/googleSignConsts.ts`: - -```typescript -export const GoogleSignInConfigure = { - scopes: ['openid', 'profile', 'email'], - offlineAccess: false, - webClientId: GOOGLE_OAUTH_CLIENT_IDS.ANDROID, // Must be Web Client ID! -}; -``` - -> **Critical**: `webClientId` must be a **Web application** type OAuth client, not Android type. This is required to receive `idToken`. - ---- - -## Web Configuration - -Web platform uses Supabase OAuth flow directly (no native Google Sign-In SDK). - -### Supabase Configuration - -1. Go to [Supabase Dashboard](https://supabase.com/dashboard) → Authentication → Providers → Google -2. Enable Google provider -3. Add Web Client ID and Client Secret -4. Configure redirect URL: `https://your-domain.com/oauth_callback_web/` - ---- - -## Browser Extension Configuration - -Extension uses `chrome.identity.launchWebAuthFlow` with the Web Client ID. - -### Google Cloud Console - -1. Create/edit Web application OAuth client -2. Add authorized redirect URI: - ``` - https://.chromiumapp.org - ``` - ---- - -## Troubleshooting - -### iOS: "Cannot read property 'SIGN_IN_CANCELLED' of null" - -**Cause**: Native module not linked properly. - -**Solution**: -1. Check `@react-native-google-signin/google-signin` is not excluded -2. Run `pod install` -3. Clean build and rebuild in Xcode - -### iOS: "DEVELOPER_ERROR" or sign-in fails silently - -**Cause**: URL scheme not configured or wrong Client ID. - -**Solution**: -1. Verify URL scheme in Info.plist matches reversed iOS Client ID -2. Verify `iosClientId` in code matches Google Cloud Console - -### Android: "DEVELOPER_ERROR" - -**Cause**: SHA-1 fingerprint mismatch or wrong Client ID. - -**Solution**: -1. Run `./gradlew signingReport` to get SHA-1 -2. Add SHA-1 to Google Cloud Console OAuth client -3. Ensure `webClientId` is a Web type client (not Android) - -### All Platforms: "No ID token received" - -**Cause**: Using wrong client type or `offlineAccess` misconfigured. - -**Solution**: -1. Ensure using **Web Client ID** for `webClientId` parameter -2. For native apps, both `webClientId` (web type) and native client must be configured - ---- - -## References - -- [Google Sign-In for iOS](https://developers.google.com/identity/sign-in/ios/start) -- [Google Sign-In for Android](https://developers.google.com/identity/sign-in/android/start) -- [@react-native-google-signin/google-signin](https://github.com/react-native-google-signin/google-signin) -- [Supabase Google Auth (React Native)](https://supabase.com/docs/guides/auth/social-login/auth-google?platform=react-native) - From 3891607cfa9303ec28ee521777bdf2f2cf283d98 Mon Sep 17 00:00:00 2001 From: morizon Date: Wed, 24 Dec 2025 22:01:57 +0800 Subject: [PATCH 19/66] fix: lint --- packages/kit/src/views/Perp/pages/Perp.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/views/Perp/pages/Perp.tsx b/packages/kit/src/views/Perp/pages/Perp.tsx index ff30b5915ee0..71d886f5a59c 100644 --- a/packages/kit/src/views/Perp/pages/Perp.tsx +++ b/packages/kit/src/views/Perp/pages/Perp.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useIsFocused } from '@react-navigation/native'; +import { useIntl } from 'react-intl'; import { Page, @@ -11,6 +12,7 @@ import { } from '@onekeyhq/components'; import { TabletHomeContainer } from '@onekeyhq/kit/src/components/TabletHomeContainer'; import { FLOAT_NAV_BAR_Z_INDEX } from '@onekeyhq/shared/src/consts/zIndexConsts'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; import { ETabRoutes } from '@onekeyhq/shared/src/routes/tab'; import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; @@ -29,8 +31,6 @@ import { PerpsProviderMirror } from '../PerpsProviderMirror'; import { ExtPerp, shouldOpenExpandExtPerp } from './ExtPerp'; import type { LayoutChangeEvent } from 'react-native'; -import { ETranslations } from '@onekeyhq/shared/src/locale'; -import { useIntl } from 'react-intl'; function PerpLayout() { const { gtMd } = useMedia(); From bd8071d44c9faf5d35069d892bf89eb340540eb5 Mon Sep 17 00:00:00 2001 From: morizon Date: Thu, 25 Dec 2025 16:05:53 +0800 Subject: [PATCH 20/66] apple login --- .../src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx | 1 + packages/shared/src/consts/authConsts.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx index e6837e1ae770..22d48481b8cc 100644 --- a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.native.tsx @@ -114,6 +114,7 @@ export class OAuthPopup extends OAuthPopupBase { } // Apple Sign-In: Use native on iOS, WebBrowser on Android + // Supabase Apple provider Client IDs must include: so.onekey.wallet (iOS Bundle ID) if (provider === 'apple') { if (platformEnv.isNativeIOS) { try { diff --git a/packages/shared/src/consts/authConsts.ts b/packages/shared/src/consts/authConsts.ts index ab0c0c38ecb3..cc3255ea7f11 100644 --- a/packages/shared/src/consts/authConsts.ts +++ b/packages/shared/src/consts/authConsts.ts @@ -127,7 +127,7 @@ export const DEFAULT_EXTENSION_OAUTH_METHOD: EExtensionOAuthMethod = export const DEFAULT_DESKTOP_OAUTH_METHOD: EDesktopOAuthMethod = EDesktopOAuthMethod.LOCALHOST_SERVER; export const DEFAULT_NATIVE_OAUTH_METHOD: ENativeOAuthMethod = - ENativeOAuthMethod.WEB_BROWSER; + ENativeOAuthMethod.NATIVE_SDK; // Google OAuth clients // - https://console.cloud.google.com/auth/clients From 0cb371771f4619bc1edba756a251bfa6948823e2 Mon Sep 17 00:00:00 2001 From: morizon Date: Thu, 25 Dec 2025 16:08:34 +0800 Subject: [PATCH 21/66] fix: add ios apple sign in entitlements --- .../OneKeyWallet/OneKeyWallet.entitlements | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/mobile/ios/OneKeyWallet/OneKeyWallet.entitlements b/apps/mobile/ios/OneKeyWallet/OneKeyWallet.entitlements index 18f7a066dac1..3c34e71a932b 100644 --- a/apps/mobile/ios/OneKeyWallet/OneKeyWallet.entitlements +++ b/apps/mobile/ios/OneKeyWallet/OneKeyWallet.entitlements @@ -4,32 +4,34 @@ aps-environment development + com.apple.developer.applesignin + + Default + com.apple.developer.associated-domains applinks:app.onekey.so - com.apple.developer.nfc.readersession.formats + com.apple.developer.icloud-container-environment + Production + com.apple.developer.icloud-container-identifiers - TAG + iCloud.so.onekey.wallet - com.apple.developer.icloud-services CloudKit CloudDocuments - com.apple.developer.icloud-container-environment - Production - com.apple.developer.ubiquity-kvstore-identifier - BVJ3FU5H2K.so.onekey.wallet - com.apple.developer.icloud-container-identifiers + com.apple.developer.nfc.readersession.formats - iCloud.so.onekey.wallet + TAG com.apple.developer.ubiquity-container-identifiers iCloud.so.onekey.wallet - + com.apple.developer.ubiquity-kvstore-identifier + BVJ3FU5H2K.so.onekey.wallet From 4cae99502e03c43f4f1138551c22f76167689311 Mon Sep 17 00:00:00 2001 From: Franco Date: Thu, 25 Dec 2025 18:04:03 +0800 Subject: [PATCH 22/66] ui --- .../Onboardingv2/pages/CreatePinPage.tsx | 11 ++- .../Onboardingv2/pages/VerifyPinPage.tsx | 86 ++++++++++++++----- packages/shared/src/routes/onboardingv2.ts | 12 ++- 3 files changed, 85 insertions(+), 24 deletions(-) diff --git a/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx index ed7dbef16e5b..a93d372c4bb8 100644 --- a/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx @@ -1,13 +1,22 @@ import { useCallback, useState } from 'react'; +import { useRoute } from '@react-navigation/core'; + import { SizableText } from '@onekeyhq/components'; +import type { IOnboardingParamListV2 } from '@onekeyhq/shared/src/routes'; import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; import useAppNavigation from '../../../hooks/useAppNavigation'; import { PinInputLayout } from '../components/PinInputLayout'; +import type { RouteProp } from '@react-navigation/core'; + function CreatePinPage() { const navigation = useAppNavigation(); + const route = + useRoute>(); + const { isResetPin } = route.params ?? {}; + const [pin, setPin] = useState(''); const handleContinue = useCallback(() => { @@ -16,7 +25,7 @@ function CreatePinPage() { return ( This is used to secure your wallet on all your devices.{' '} diff --git a/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx index c2515053e652..402049ebf00f 100644 --- a/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx @@ -1,10 +1,15 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useRoute } from '@react-navigation/core'; + +import type { IOnboardingParamListV2 } from '@onekeyhq/shared/src/routes'; import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; import useAppNavigation from '../../../hooks/useAppNavigation'; import { PinInputLayout } from '../components/PinInputLayout'; +import type { RouteProp } from '@react-navigation/core'; + const MAX_ATTEMPTS = 7; // Cooldown times based on attempt number (in seconds) @@ -20,6 +25,12 @@ const COOLDOWN_BY_ATTEMPT: Record = { function VerifyPinPage() { const navigation = useAppNavigation(); + const route = + useRoute>(); + const { verifyType = 'periodic' } = route.params ?? {}; + + const isSocialLogin = verifyType === 'socialLogin'; + const [pin, setPin] = useState(''); const [errorMessage, setErrorMessage] = useState(''); const [attemptsRemaining, setAttemptsRemaining] = useState(MAX_ATTEMPTS); @@ -27,7 +38,22 @@ function VerifyPinPage() { const [showAttemptError, setShowAttemptError] = useState(false); const cooldownTimerRef = useRef | null>(null); - const isInputDisabled = cooldownSeconds > 0; + const isInputDisabled = isSocialLogin && cooldownSeconds > 0; + + const { title, description } = useMemo(() => { + if (isSocialLogin) { + return { + title: 'Enter your PIN', + description: + 'This email already has a wallet created. Please enter your PIN to login.', + }; + } + return { + title: 'Remember your PIN?', + description: + "Just a friendly reminder to keep your PIN fresh in memory. We'll check in from time to time.", + }; + }, [isSocialLogin]); // Clear cooldown timer on unmount useEffect(() => { @@ -82,31 +108,47 @@ function VerifyPinPage() { const isCorrect = false; // Mock: always fail for testing if (isCorrect) { - navigation.push(EOnboardingPagesV2.CreatePasscode); + if (isSocialLogin) { + navigation.push(EOnboardingPagesV2.CreatePasscode); + } else { + // For periodic verification, just go back + navigation.pop(); + } } else { - const newAttemptsRemaining = attemptsRemaining - 1; - const attemptNumber = MAX_ATTEMPTS - newAttemptsRemaining; - - setAttemptsRemaining(newAttemptsRemaining); setPin(''); - setShowAttemptError(true); - if (newAttemptsRemaining <= 0) { - // Max attempts reached - redirect to reset PIN page - navigation.replace(EOnboardingPagesV2.ResetPin); - } else { - // Get cooldown time for this attempt - const cooldownTime = COOLDOWN_BY_ATTEMPT[attemptNumber] || 0; - if (cooldownTime > 0) { - startCooldown(cooldownTime); + if (isSocialLogin) { + // Social login: apply retry mechanism with cooldown + const newAttemptsRemaining = attemptsRemaining - 1; + const attemptNumber = MAX_ATTEMPTS - newAttemptsRemaining; + + setAttemptsRemaining(newAttemptsRemaining); + setShowAttemptError(true); + + if (newAttemptsRemaining <= 0) { + // Max attempts reached - redirect to reset PIN page + navigation.replace(EOnboardingPagesV2.ResetPin); + } else { + // Get cooldown time for this attempt + const cooldownTime = COOLDOWN_BY_ATTEMPT[attemptNumber] || 0; + if (cooldownTime > 0) { + startCooldown(cooldownTime); + } } + } else { + // Periodic verification: simple error message, no retry mechanism + setErrorMessage('Incorrect PIN. Please try again.'); } } - }, [attemptsRemaining, navigation, startCooldown]); + }, [attemptsRemaining, isSocialLogin, navigation, startCooldown]); const handleForgotPin = useCallback(() => { - navigation.push(EOnboardingPagesV2.ResetPin); - }, [navigation]); + if (isSocialLogin) { + navigation.push(EOnboardingPagesV2.ResetPin); + } else { + navigation.push(EOnboardingPagesV2.CreatePin, { isResetPin: true }); + } + }, [isSocialLogin, navigation]); // Build error message based on state const displayErrorMessage = (() => { @@ -114,6 +156,7 @@ function VerifyPinPage() { return errorMessage; } if ( + isSocialLogin && showAttemptError && attemptsRemaining < MAX_ATTEMPTS && attemptsRemaining > 0 @@ -131,9 +174,10 @@ function VerifyPinPage() { return ( Date: Thu, 25 Dec 2025 18:16:20 +0800 Subject: [PATCH 23/66] apple native login --- .../app/service/appleAuth/appleAuth.ts | 80 +++++ .../native-modules/apple-auth-macos/README.md | 81 +++++ .../apple-auth-macos/binding.gyp | 30 ++ .../apple-auth-macos/index.d.ts | 33 ++ .../native-modules/apple-auth-macos/index.js | 76 +++++ .../apple-auth-macos/package.json | 16 + .../apple-auth-macos/src/apple_auth.mm | 317 ++++++++++++++++++ .../src/desktopApis/DesktopApiAppleAuth.ts | 55 +++ .../src/desktopApis/instance/IDesktopApi.ts | 2 + .../src/desktopApis/instance/desktopApi.ts | 5 + .../desktopApis/instance/desktopApiProxy.ts | 4 + .../OAuthPopup/OAuthPopup.desktop.tsx | 158 ++++++++- packages/shared/src/consts/authConsts.ts | 6 + 13 files changed, 858 insertions(+), 5 deletions(-) create mode 100644 apps/desktop/app/service/appleAuth/appleAuth.ts create mode 100644 apps/desktop/native-modules/apple-auth-macos/README.md create mode 100644 apps/desktop/native-modules/apple-auth-macos/binding.gyp create mode 100644 apps/desktop/native-modules/apple-auth-macos/index.d.ts create mode 100644 apps/desktop/native-modules/apple-auth-macos/index.js create mode 100644 apps/desktop/native-modules/apple-auth-macos/package.json create mode 100644 apps/desktop/native-modules/apple-auth-macos/src/apple_auth.mm create mode 100644 packages/kit-bg/src/desktopApis/DesktopApiAppleAuth.ts diff --git a/apps/desktop/app/service/appleAuth/appleAuth.ts b/apps/desktop/app/service/appleAuth/appleAuth.ts new file mode 100644 index 000000000000..ec571575de35 --- /dev/null +++ b/apps/desktop/app/service/appleAuth/appleAuth.ts @@ -0,0 +1,80 @@ +/** + * Native Apple Sign-In service for macOS Desktop + * + * Uses the native apple-auth-macos module to perform Apple Sign-In + * with system UI (no browser required). + */ + +import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; + +// eslint-disable-next-line import/no-relative-packages +import type { IAppleSignInResult } from '../../../native-modules/apple-auth-macos'; + +// Only load on macOS +const isMacOS = process.platform === 'darwin'; + +let appleAuthModule: + | typeof import('../../../native-modules/apple-auth-macos') + | null = null; + +// Lazy load the native module +function getAppleAuthModule() { + if (!isMacOS) { + return null; + } + + if (!appleAuthModule) { + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + appleAuthModule = require('../../native-modules/apple-auth-macos'); + } catch (error) { + console.warn( + 'Failed to load apple-auth-macos:', + error instanceof Error ? error.message : error, + ); + return null; + } + } + + return appleAuthModule; +} + +/** + * Check if native Apple Sign-In is available. + * Requires macOS 10.15+ and the native module to be built. + */ +export function isAppleAuthAvailable(): boolean { + const module = getAppleAuthModule(); + if (!module) { + return false; + } + + try { + return module.isAvailable(); + } catch { + return false; + } +} + +/** + * Perform native Apple Sign-In. + * + * @returns Promise with identity token and nonce for Supabase + * @throws Error if sign-in fails or user cancels + */ +export async function signInWithApple(): Promise { + const module = getAppleAuthModule(); + + if (!module) { + throw new OneKeyLocalError( + 'Apple Sign-In native module is not available. ' + + 'Make sure you are on macOS and the module is built.', + ); + } + + if (!module.isAvailable()) { + throw new OneKeyLocalError('Apple Sign-In requires macOS 10.15 or later'); + } + + return module.signIn(); +} diff --git a/apps/desktop/native-modules/apple-auth-macos/README.md b/apps/desktop/native-modules/apple-auth-macos/README.md new file mode 100644 index 000000000000..991bc611f408 --- /dev/null +++ b/apps/desktop/native-modules/apple-auth-macos/README.md @@ -0,0 +1,81 @@ +# apple-auth-macos + +Native Apple Sign-In for macOS Electron apps. + +## Requirements + +- macOS 10.15 (Catalina) or later +- Xcode Command Line Tools +- Node.js with node-gyp + +## Build + +```bash +cd apps/desktop/native-modules/apple-auth-macos +npm install +npm run build +``` + +Or from project root: + +```bash +node apps/desktop/development/build_apple_auth.js +``` + +## Usage + +```javascript +const appleAuth = require('./native-modules/apple-auth-macos'); + +// Check availability +if (appleAuth.isAvailable()) { + try { + const result = await appleAuth.signIn(); + console.log('Identity Token:', result.identityToken); + console.log('Raw Nonce:', result.rawNonce); + + // Use with Supabase + const { data, error } = await supabase.auth.signInWithIdToken({ + provider: 'apple', + token: result.identityToken, + nonce: result.rawNonce, + }); + } catch (error) { + if (error.message.includes('cancelled')) { + console.log('User cancelled'); + } else { + console.error('Sign-in failed:', error); + } + } +} +``` + +## API + +### `isAvailable(): boolean` + +Check if Apple Sign-In is available on this system. + +### `signIn(): Promise` + +Perform Apple Sign-In. Returns: + +```typescript +interface AppleSignInResult { + identityToken: string; // JWT to send to Supabase + authorizationCode?: string; + user: string; // Apple user ID + email?: string; // Only on first sign-in + fullName?: string; // Only on first sign-in + rawNonce: string; // For Supabase nonce validation +} +``` + +## Important Notes + +1. **Supabase Configuration**: Make sure your Supabase Apple provider has the correct bundle ID configured in "Client ID(s)". + +2. **App Signing**: For production, the app must be properly signed with Apple Developer certificate and have the "Sign in with Apple" capability. + +3. **First Sign-In**: Email and full name are only provided on the user's first sign-in with your app. Store them if needed. + diff --git a/apps/desktop/native-modules/apple-auth-macos/binding.gyp b/apps/desktop/native-modules/apple-auth-macos/binding.gyp new file mode 100644 index 000000000000..511de940e2f9 --- /dev/null +++ b/apps/desktop/native-modules/apple-auth-macos/binding.gyp @@ -0,0 +1,30 @@ +{ + "targets": [ + { + "target_name": "apple_auth", + "conditions": [ + ["OS=='mac'", { + "sources": [ + "src/apple_auth.mm" + ], + "include_dirs": [ + "; diff --git a/apps/desktop/native-modules/apple-auth-macos/index.js b/apps/desktop/native-modules/apple-auth-macos/index.js new file mode 100644 index 000000000000..e20a1eada433 --- /dev/null +++ b/apps/desktop/native-modules/apple-auth-macos/index.js @@ -0,0 +1,76 @@ +/** + * Native Apple Sign-In for macOS Electron apps + * + * Usage: + * const appleAuth = require('apple-auth-macos'); + * + * if (appleAuth.isAvailable()) { + * const result = await appleAuth.signIn(); + * console.log(result.identityToken); // JWT to send to Supabase + * } + */ + +// Only load native module on macOS +const isMacOS = process.platform === 'darwin'; + +let nativeModule = null; + +if (isMacOS) { + try { + // Try to load the native module + // In development: from build/Release/ + // In production: from the app bundle + nativeModule = require('./build/Release/apple_auth.node'); + } catch (error) { + console.warn( + 'Failed to load apple-auth-macos native module:', + error.message, + ); + } +} + +/** + * Check if Apple Sign-In is available. + * @returns {boolean} True if available (macOS 10.15+) + */ +function isAvailable() { + if (!isMacOS || !nativeModule) { + return false; + } + try { + return nativeModule.isAvailable(); + } catch { + return false; + } +} + +/** + * Perform Apple Sign-In. + * @returns {Promise<{ + * identityToken: string, + * authorizationCode?: string, + * user: string, + * email?: string, + * fullName?: string, + * rawNonce: string + * }>} + * @throws {Error} If sign-in fails or is cancelled + */ +async function signIn() { + if (!isMacOS) { + throw new Error('Apple Sign-In is only available on macOS'); + } + if (!nativeModule) { + throw new Error('Apple Sign-In native module not loaded'); + } + if (!isAvailable()) { + throw new Error('Apple Sign-In requires macOS 10.15 or later'); + } + + return nativeModule.signIn(); +} + +module.exports = { + isAvailable, + signIn, +}; diff --git a/apps/desktop/native-modules/apple-auth-macos/package.json b/apps/desktop/native-modules/apple-auth-macos/package.json new file mode 100644 index 000000000000..2f68ae587081 --- /dev/null +++ b/apps/desktop/native-modules/apple-auth-macos/package.json @@ -0,0 +1,16 @@ +{ + "name": "apple-auth-macos", + "version": "1.0.0", + "description": "Native Apple Sign-In for macOS Electron apps", + "main": "index.js", + "private": true, + "scripts": { + "build": "node-gyp rebuild", + "clean": "node-gyp clean" + }, + "dependencies": { + "node-addon-api": "^7.0.0" + }, + "gypfile": true +} + diff --git a/apps/desktop/native-modules/apple-auth-macos/src/apple_auth.mm b/apps/desktop/native-modules/apple-auth-macos/src/apple_auth.mm new file mode 100644 index 000000000000..cbb46295dbee --- /dev/null +++ b/apps/desktop/native-modules/apple-auth-macos/src/apple_auth.mm @@ -0,0 +1,317 @@ +/** + * Native Apple Sign-In for macOS Electron apps + * + * Uses ASAuthorizationController from AuthenticationServices framework + * to perform native Apple Sign-In and return the identity token. + * + * Reference: + * - https://developer.apple.com/documentation/authenticationservices/asauthorizationcontroller + * - https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidprovider + */ + +#import +#import +#import +#import +#import + +// ============================================================================ +// Helper Functions +// ============================================================================ + +// Helper: Generate SHA256 hash of a string (for nonce) +static NSString* sha256(NSString* input) { + const char* str = [input UTF8String]; + unsigned char hash[CC_SHA256_DIGEST_LENGTH]; + CC_SHA256(str, (CC_LONG)strlen(str), hash); + + NSMutableString* output = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2]; + for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) { + [output appendFormat:@"%02x", hash[i]]; + } + return output; +} + +// Helper: Generate a random nonce string +static NSString* generateNonce(int length) { + NSMutableString* nonce = [NSMutableString stringWithCapacity:length]; + NSString* chars = @"0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._"; + for (int i = 0; i < length; i++) { + uint32_t index = arc4random_uniform((uint32_t)[chars length]); + [nonce appendFormat:@"%C", [chars characterAtIndex:index]]; + } + return nonce; +} + +// ============================================================================ +// Result Data Structure (thread-safe, plain C++ data) +// ============================================================================ + +struct AppleAuthResult { + bool success; + std::string errorMessage; + std::string identityToken; + std::string authorizationCode; + std::string user; + std::string email; + std::string fullName; + std::string rawNonce; +}; + +// ============================================================================ +// Delegate class for ASAuthorizationController +// ============================================================================ + +API_AVAILABLE(macos(10.15)) +@interface AppleAuthDelegate : NSObject +@property (nonatomic, copy) void (^completionHandler)(AppleAuthResult result); +@property (nonatomic, strong) NSString* rawNonce; +@end + +@implementation AppleAuthDelegate + +- (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(macos(10.15)) { + // Return the key window as presentation anchor + return [[NSApplication sharedApplication] keyWindow] ?: [[NSApplication sharedApplication] mainWindow]; +} + +- (void)authorizationController:(ASAuthorizationController *)controller + didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(macos(10.15)) { + AppleAuthResult result = {}; + result.success = false; + + if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) { + ASAuthorizationAppleIDCredential* credential = (ASAuthorizationAppleIDCredential*)authorization.credential; + + result.success = true; + + if (credential.identityToken) { + NSString* token = [[NSString alloc] initWithData:credential.identityToken encoding:NSUTF8StringEncoding]; + if (token) { + result.identityToken = [token UTF8String]; + } + } + + if (credential.authorizationCode) { + NSString* code = [[NSString alloc] initWithData:credential.authorizationCode encoding:NSUTF8StringEncoding]; + if (code) { + result.authorizationCode = [code UTF8String]; + } + } + + if (credential.user) { + result.user = [credential.user UTF8String]; + } + + if (credential.email) { + result.email = [credential.email UTF8String]; + } + + if (credential.fullName) { + NSPersonNameComponentsFormatter* formatter = [[NSPersonNameComponentsFormatter alloc] init]; + formatter.style = NSPersonNameComponentsFormatterStyleDefault; + NSString* fullName = [formatter stringFromPersonNameComponents:credential.fullName]; + if (fullName && [fullName length] > 0) { + result.fullName = [fullName UTF8String]; + } + } + + if (self.rawNonce) { + result.rawNonce = [self.rawNonce UTF8String]; + } + } else { + result.errorMessage = "Unknown credential type"; + } + + if (self.completionHandler) { + self.completionHandler(result); + } +} + +- (void)authorizationController:(ASAuthorizationController *)controller + didCompleteWithError:(NSError *)error API_AVAILABLE(macos(10.15)) { + AppleAuthResult result = {}; + result.success = false; + + if (error.code == ASAuthorizationErrorCanceled) { + result.errorMessage = "User cancelled Apple Sign-In"; + } else { + result.errorMessage = [[NSString stringWithFormat:@"Apple Sign-In failed: %@", error.localizedDescription] UTF8String]; + } + + if (self.completionHandler) { + self.completionHandler(result); + } +} + +@end + +// ============================================================================ +// Async Worker for Apple Sign-In +// ============================================================================ + +class AppleSignInWorker : public Napi::AsyncWorker { +public: + AppleSignInWorker(Napi::Env env, Napi::Promise::Deferred deferred) + : Napi::AsyncWorker(env), deferred_(deferred) {} + + void Execute() override { + // This runs in a worker thread, but we need to call Apple APIs on main thread + // So we use dispatch_sync to run on main thread and wait for result + + if (@available(macOS 10.15, *)) { + __block AppleAuthResult blockResult = {}; + __block bool completed = false; + __block NSCondition* condition = [[NSCondition alloc] init]; + + // Generate nonce + NSString* rawNonce = generateNonce(32); + NSString* hashedNonce = sha256(rawNonce); + + dispatch_async(dispatch_get_main_queue(), ^{ + // Create Apple ID provider and request + ASAuthorizationAppleIDProvider* provider = [[ASAuthorizationAppleIDProvider alloc] init]; + ASAuthorizationAppleIDRequest* request = [provider createRequest]; + request.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail]; + request.nonce = hashedNonce; + + // Create authorization controller + ASAuthorizationController* controller = [[ASAuthorizationController alloc] + initWithAuthorizationRequests:@[request]]; + + // Create delegate + AppleAuthDelegate* delegate = [[AppleAuthDelegate alloc] init]; + delegate.rawNonce = rawNonce; + + // Store delegate to prevent deallocation + static AppleAuthDelegate* gDelegate = nil; + gDelegate = delegate; + + delegate.completionHandler = ^(AppleAuthResult res) { + [condition lock]; + blockResult = res; + completed = true; + [condition signal]; + [condition unlock]; + gDelegate = nil; // Release delegate + }; + + controller.delegate = delegate; + controller.presentationContextProvider = delegate; + + // Perform authorization request + [controller performRequests]; + }); + + // Wait for completion (with timeout) + [condition lock]; + NSDate* timeout = [NSDate dateWithTimeIntervalSinceNow:300]; // 5 minutes + while (!completed) { + if (![condition waitUntilDate:timeout]) { + // Timeout + blockResult.success = false; + blockResult.errorMessage = "Apple Sign-In timed out"; + break; + } + } + [condition unlock]; + + result_ = blockResult; + } else { + result_.success = false; + result_.errorMessage = "Apple Sign-In requires macOS 10.15 or later"; + } + } + + void OnOK() override { + Napi::Env env = Env(); + + if (result_.success) { + Napi::Object obj = Napi::Object::New(env); + obj.Set("identityToken", Napi::String::New(env, result_.identityToken)); + + if (!result_.authorizationCode.empty()) { + obj.Set("authorizationCode", Napi::String::New(env, result_.authorizationCode)); + } + if (!result_.user.empty()) { + obj.Set("user", Napi::String::New(env, result_.user)); + } + if (!result_.email.empty()) { + obj.Set("email", Napi::String::New(env, result_.email)); + } + if (!result_.fullName.empty()) { + obj.Set("fullName", Napi::String::New(env, result_.fullName)); + } + if (!result_.rawNonce.empty()) { + obj.Set("rawNonce", Napi::String::New(env, result_.rawNonce)); + } + + deferred_.Resolve(obj); + } else { + deferred_.Reject(Napi::Error::New(env, result_.errorMessage).Value()); + } + } + + void OnError(const Napi::Error& e) override { + deferred_.Reject(e.Value()); + } + +private: + Napi::Promise::Deferred deferred_; + AppleAuthResult result_; +}; + +// ============================================================================ +// N-API Functions +// ============================================================================ + +/** + * Check if Apple Sign-In is available on this system. + * Requires macOS 10.15 (Catalina) or later. + */ +Napi::Boolean IsAvailable(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + + if (@available(macOS 10.15, *)) { + return Napi::Boolean::New(env, true); + } + return Napi::Boolean::New(env, false); +} + +/** + * Perform Apple Sign-In and return identity token. + * + * Returns a Promise that resolves with: + * { + * identityToken: string, // JWT token to send to Supabase + * authorizationCode: string, + * user: string, // Apple user ID + * email: string | null, // Only on first sign-in + * fullName: string | null, // Only on first sign-in + * rawNonce: string // Raw nonce for Supabase validation + * } + * + * Or rejects with an error. + */ +Napi::Value SignIn(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env); + + if (@available(macOS 10.15, *)) { + AppleSignInWorker* worker = new AppleSignInWorker(env, deferred); + worker->Queue(); + } else { + deferred.Reject(Napi::Error::New(env, "Apple Sign-In requires macOS 10.15 or later").Value()); + } + + return deferred.Promise(); +} + +// Module initialization +Napi::Object Init(Napi::Env env, Napi::Object exports) { + exports.Set("isAvailable", Napi::Function::New(env, IsAvailable)); + exports.Set("signIn", Napi::Function::New(env, SignIn)); + return exports; +} + +NODE_API_MODULE(apple_auth, Init) diff --git a/packages/kit-bg/src/desktopApis/DesktopApiAppleAuth.ts b/packages/kit-bg/src/desktopApis/DesktopApiAppleAuth.ts new file mode 100644 index 000000000000..107313ed32ad --- /dev/null +++ b/packages/kit-bg/src/desktopApis/DesktopApiAppleAuth.ts @@ -0,0 +1,55 @@ +import { + isAppleAuthAvailable, + signInWithApple, +} from '@onekeyhq/desktop/app/service/appleAuth/appleAuth'; + +import type { IDesktopApi } from './instance/IDesktopApi'; + +export interface IAppleSignInResult { + /** JWT identity token to send to Supabase signInWithIdToken */ + identityToken: string; + /** Authorization code (for server-side validation) */ + authorizationCode?: string; + /** Apple user identifier (stable across sign-ins) */ + user: string; + /** User's email (only provided on first sign-in) */ + email?: string; + /** User's full name (only provided on first sign-in) */ + fullName?: string; + /** Raw nonce for Supabase validation */ + rawNonce: string; +} + +/** + * Desktop API for native Apple Sign-In (macOS only) + * + * Uses ASAuthorizationController from AuthenticationServices framework + * to perform native Apple Sign-In without opening a browser. + */ +class DesktopApiAppleAuth { + constructor({ desktopApi }: { desktopApi: IDesktopApi }) { + this.desktopApi = desktopApi; + } + + desktopApi: IDesktopApi; + + /** + * Check if native Apple Sign-In is available. + * Requires macOS 10.15+ and the native module to be built. + */ + isAvailable(): boolean { + return isAppleAuthAvailable(); + } + + /** + * Perform native Apple Sign-In. + * + * @returns Promise with identity token and nonce for Supabase + * @throws Error if sign-in fails or user cancels + */ + async signIn(): Promise { + return signInWithApple(); + } +} + +export default DesktopApiAppleAuth; diff --git a/packages/kit-bg/src/desktopApis/instance/IDesktopApi.ts b/packages/kit-bg/src/desktopApis/instance/IDesktopApi.ts index 08342ebee447..5b90c1d5a6a3 100644 --- a/packages/kit-bg/src/desktopApis/instance/IDesktopApi.ts +++ b/packages/kit-bg/src/desktopApis/instance/IDesktopApi.ts @@ -1,3 +1,4 @@ +import type DesktopApiAppleAuth from '../DesktopApiAppleAuth'; import type DesktopApiAppUpdate from '../DesktopApiAppUpdate'; import type DesktopApiBluetooth from '../DesktopApiBluetooth'; import type DesktopApiBundleUpdate from '../DesktopApiBundleUpdate'; @@ -28,4 +29,5 @@ export interface IDesktopApi { keychain: DesktopApiKeychain; sniRequest: DesktopApiSniRequest; oauthLocalServer: DesktopApiOAuthLocalServer; + appleAuth: DesktopApiAppleAuth; } diff --git a/packages/kit-bg/src/desktopApis/instance/desktopApi.ts b/packages/kit-bg/src/desktopApis/instance/desktopApi.ts index cd87f21c87b1..eada09d1b280 100644 --- a/packages/kit-bg/src/desktopApis/instance/desktopApi.ts +++ b/packages/kit-bg/src/desktopApis/instance/desktopApi.ts @@ -4,6 +4,7 @@ import { memoizee } from '@onekeyhq/shared/src/utils/cacheUtils'; import { DESKTOP_API_MESSAGE_TYPE } from '../base/consts'; import { JsBridgeDesktopApiOfMain } from '../base/JsBridgeDesktopApiOfMain'; +import DesktopApiAppleAuth from '../DesktopApiAppleAuth'; import DesktopApiAppUpdate from '../DesktopApiAppUpdate'; import DesktopApiBluetooth from '../DesktopApiBluetooth'; import DesktopApiBundleUpdate from '../DesktopApiBundleUpdate'; @@ -83,6 +84,10 @@ class DesktopApi implements IDesktopApi { desktopApi: this, }, ); + + appleAuth: DesktopApiAppleAuth = new DesktopApiAppleAuth({ + desktopApi: this, + }); } const desktopApi = new DesktopApi(); diff --git a/packages/kit-bg/src/desktopApis/instance/desktopApiProxy.ts b/packages/kit-bg/src/desktopApis/instance/desktopApiProxy.ts index dbd5967b9e77..86cf98a6a830 100644 --- a/packages/kit-bg/src/desktopApis/instance/desktopApiProxy.ts +++ b/packages/kit-bg/src/desktopApis/instance/desktopApiProxy.ts @@ -10,6 +10,7 @@ import type { IDesktopApiKeys, IDesktopApiMessagePayload, } from '../base/types'; +import type DesktopApiAppleAuth from '../DesktopApiAppleAuth'; import type DesktopApiAppUpdate from '../DesktopApiAppUpdate'; import type DesktopApiBluetooth from '../DesktopApiBluetooth'; import type DesktopApiBundleUpdate from '../DesktopApiBundleUpdate'; @@ -97,6 +98,9 @@ export class DesktopApiProxy extends RemoteApiProxyBase implements IDesktopApi { oauthLocalServer: DesktopApiOAuthLocalServer = this._createProxyModule('oauthLocalServer'); + + appleAuth: DesktopApiAppleAuth = + this._createProxyModule('appleAuth'); } const desktopApiProxy = new DesktopApiProxy(); diff --git a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.desktop.tsx b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.desktop.tsx index e0bd1f30e04f..51e6388d5e62 100644 --- a/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.desktop.tsx +++ b/packages/kit/src/components/OneKeyAuth/OAuthPopup/OAuthPopup.desktop.tsx @@ -1,6 +1,8 @@ import { Dialog } from '@onekeyhq/components'; import type { IDialogInstance } from '@onekeyhq/components'; +import type { IAppleSignInResult } from '@onekeyhq/kit-bg/src/desktopApis/DesktopApiAppleAuth'; import { + MAC_DESKTOP_USE_NATIVE_APPLE_SIGNIN, OAUTH_CALLBACK_DESKTOP_CHANNEL, OAUTH_CALLBACK_DESKTOP_PATH, OAUTH_FLOW_TIMEOUT_MS, @@ -19,10 +21,11 @@ import type { IOAuthPopupOptions, IOAuthPopupResult } from './types'; /** * OAuth popup implementation for Desktop (Electron) platform. * - * Uses localhost HTTP server for OAuth callback (primary method). - * Opens OAuth URL in system browser and listens for callback via IPC. + * Supports two methods: + * - Native Apple Sign-In (macOS only): Uses ASAuthorizationController for native UI + * - Browser OAuth (default): Opens system browser with localhost callback * - * Flow: + * Flow (Browser method): * 1. Start localhost HTTP server on system-assigned port * 2. Open Supabase OAuth URL in system browser * 3. User completes OAuth in browser @@ -67,12 +70,157 @@ export class OAuthPopup extends OAuthPopupBase { } /** - * Open OAuth using localhost HTTP server. + * Open OAuth popup. * - * Opens OAuth URL in system browser and listens for callback via IPC. + * Routes to the appropriate sign-in method based on provider and platform: + * - Apple on macOS: Uses native Apple Sign-In via DesktopApiAppleAuth (if available) + * - Other providers: Uses browser OAuth with localhost callback */ static override async open( options: IOAuthPopupOptions, + ): Promise { + const { provider, client } = options; + + if (!client) { + throw new OneKeyLocalError('Supabase client is required'); + } + + // Apple Sign-In on macOS: Try native method first (if enabled) + if ( + provider === 'apple' && + platformEnv.isDesktopMac && + MAC_DESKTOP_USE_NATIVE_APPLE_SIGNIN + ) { + try { + // TODO: macOS Native Apple Sign-In requirements: + // 1. Build native module: cd apps/desktop/native-modules/apple-auth-macos && npx node-gyp rebuild + // 2. Add entitlement to apps/desktop/entitlements.mac.plist and entitlements.mas.plist: + // com.apple.developer.applesignin + // Default + // 3. App must be code signed with proper provisioning profile + // 4. Apple Developer account must have "Sign in with Apple" capability enabled + // 5. Supabase Apple provider must include the app's bundle ID in "Client IDs" + + return await OAuthPopup.openWithNativeAppleSignIn(options); + } catch (error) { + // If native Apple Sign-In fails due to module not available, fall back to browser + if (OAuthPopup.shouldFallbackToBrowser(error)) { + console.warn( + 'Native Apple Sign-In not available, falling back to browser:', + error instanceof Error ? error.message : error, + ); + // Fall through to browser method + } else { + throw error; + } + } + } + + // Browser OAuth method + return OAuthPopup.openWithBrowser(options); + } + + // ============ Private Methods - Native Apple Sign-In ============ + + /** + * Check if error indicates native Apple Sign-In is not available + * and we should fall back to browser. + */ + private static shouldFallbackToBrowser(error: unknown): boolean { + if (error instanceof Error) { + const message = error.message.toLowerCase(); + return ( + message.includes('not available') || + message.includes('not loaded') || + message.includes('native module') + ); + } + return false; + } + + /** + * Open OAuth using native Apple Sign-In (macOS only). + * + * Uses DesktopApiAppleAuth to perform native Apple Sign-In + * with ASAuthorizationController (system UI, no browser). + */ + private static async openWithNativeAppleSignIn( + options: IOAuthPopupOptions, + ): Promise { + const { client, handleSessionPersistence } = options; + + if (!client) { + throw new OneKeyLocalError('Supabase client is required'); + } + + if (!globalThis.desktopApiProxy?.appleAuth) { + throw new OneKeyLocalError( + 'Desktop Apple Auth API is not available. Native module may not be built.', + ); + } + + // Check if native Apple Sign-In is available + const appleAuth = globalThis.desktopApiProxy.appleAuth as { + isAvailable: () => boolean; + signIn: () => Promise; + }; + + const isAvailable = appleAuth.isAvailable(); + if (!isAvailable) { + throw new OneKeyLocalError( + 'Native Apple Sign-In requires macOS 10.15 or later and the native module to be built.', + ); + } + + // Perform native Apple Sign-In + const result: IAppleSignInResult = await appleAuth.signIn(); + + if (!result.identityToken) { + throw new OneKeyLocalError( + 'No identity token received from Apple Sign-In', + ); + } + + // Exchange Apple ID token for Supabase session + // The raw nonce is passed to Supabase to validate against the hashed nonce in the ID token + const { data, error } = await client.auth.signInWithIdToken({ + provider: 'apple', + token: result.identityToken, + nonce: result.rawNonce, + }); + + if (error) { + throw new OneKeyLocalError(error.message); + } + + if (!data.session) { + throw new OneKeyLocalError( + 'Failed to exchange Apple ID token for session', + ); + } + + const accessToken = data.session.access_token; + const refreshToken = data.session.refresh_token; + + // Handle session persistence + await handleSessionPersistence({ + accessToken, + refreshToken, + }); + + return { + success: true, + session: { accessToken, refreshToken }, + }; + } + + // ============ Private Methods - Browser OAuth ============ + + /** + * Open OAuth using localhost HTTP server and system browser. + */ + private static async openWithBrowser( + options: IOAuthPopupOptions, ): Promise { const { authUrl, client, handleSessionPersistence } = options; diff --git a/packages/shared/src/consts/authConsts.ts b/packages/shared/src/consts/authConsts.ts index cc3255ea7f11..62160e1a57f6 100644 --- a/packages/shared/src/consts/authConsts.ts +++ b/packages/shared/src/consts/authConsts.ts @@ -118,6 +118,12 @@ export const EXTENSION_OAUTH_USE_PKCE_FLOW = true; // Reference: https://developer.apple.com/documentation/authenticationservices/asauthorizationopenidrequest/nonce export const APPLE_SIGNIN_USE_NONCE = true; +// Desktop native Apple Sign-In (macOS only) +// When enabled, macOS will use native ASAuthorizationController for Apple Sign-In +// instead of opening the browser. Provides better UX with system UI and Touch ID. +// Set to false to always use browser OAuth flow. +export const MAC_DESKTOP_USE_NATIVE_APPLE_SIGNIN = false; + // Email OTP export const EMAIL_OTP_COUNTDOWN_SECONDS = 60; From c3098f632dac45b22c5b83b08ff4775d4b0fe24d Mon Sep 17 00:00:00 2001 From: Franco Date: Fri, 26 Dec 2025 10:24:03 +0800 Subject: [PATCH 24/66] ui --- packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx index 402049ebf00f..b6260183149e 100644 --- a/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx @@ -27,7 +27,7 @@ function VerifyPinPage() { const navigation = useAppNavigation(); const route = useRoute>(); - const { verifyType = 'periodic' } = route.params ?? {}; + const { verifyType = 'socialLogin' } = route.params ?? {}; const isSocialLogin = verifyType === 'socialLogin'; From 6b0cce34b1306b1916c9629a528c437dfc0ba4fa Mon Sep 17 00:00:00 2001 From: morizon Date: Fri, 26 Dec 2025 11:43:17 +0800 Subject: [PATCH 25/66] feat: add Keyless Wallet V2 functionality and update onboarding flow --- packages/kit-bg/src/dbs/local/consts.ts | 2 +- .../local/realm/schemas/RealmSchemaWallet.ts | 4 +++ packages/kit-bg/src/dbs/local/types.ts | 1 + .../services/ServiceAccount/ServiceAccount.ts | 9 +++++ .../KeylessWallet/useKeylessWallet.tsx | 36 +++++++++++++++++++ .../Password/components/PasswordSetup.tsx | 2 +- packages/kit/src/provider/Bootstrap.tsx | 23 ++++++------ .../pages/CreateOrImportWallet.tsx | 26 +++++++++++--- 8 files changed, 86 insertions(+), 17 deletions(-) diff --git a/packages/kit-bg/src/dbs/local/consts.ts b/packages/kit-bg/src/dbs/local/consts.ts index fb93816f819c..4ab06c7986df 100644 --- a/packages/kit-bg/src/dbs/local/consts.ts +++ b/packages/kit-bg/src/dbs/local/consts.ts @@ -9,7 +9,7 @@ export const IS_DB_BUCKET_SUPPORT = Boolean( ); const LOCAL_DB_NAME = 'OneKeyV5'; -const LOCAL_DB_VERSION = 14; +const LOCAL_DB_VERSION = 15; // ---------------------------------------------- diff --git a/packages/kit-bg/src/dbs/local/realm/schemas/RealmSchemaWallet.ts b/packages/kit-bg/src/dbs/local/realm/schemas/RealmSchemaWallet.ts index 53a5783531e4..e4d1525d798c 100644 --- a/packages/kit-bg/src/dbs/local/realm/schemas/RealmSchemaWallet.ts +++ b/packages/kit-bg/src/dbs/local/realm/schemas/RealmSchemaWallet.ts @@ -30,6 +30,8 @@ class RealmSchemaWallet extends RealmObjectBase { public isMocked?: boolean; + public isKeyless?: boolean; + public passphraseState?: string; public firstEvmAddress?: string; @@ -64,6 +66,7 @@ class RealmSchemaWallet extends RealmObjectBase { associatedDevice: 'string?', isTemp: { type: 'bool', default: false }, isMocked: { type: 'bool', default: false }, + isKeyless: { type: 'bool', default: false }, passphraseState: 'string?', firstEvmAddress: 'string?', hash: 'string?', @@ -90,6 +93,7 @@ class RealmSchemaWallet extends RealmObjectBase { associatedDevice: this.associatedDevice, isTemp: this.isTemp, isMocked: this.isMocked, + isKeyless: this.isKeyless, passphraseState: this.passphraseState, firstEvmAddress: this.firstEvmAddress, hash: this.hash, diff --git a/packages/kit-bg/src/dbs/local/types.ts b/packages/kit-bg/src/dbs/local/types.ts index efe4370c85cf..4c1134caeae0 100644 --- a/packages/kit-bg/src/dbs/local/types.ts +++ b/packages/kit-bg/src/dbs/local/types.ts @@ -155,6 +155,7 @@ export type IDBWallet = IDBBaseObjectWithName & { dbIndexedAccounts?: IDBIndexedAccount[]; // readonly field isTemp?: boolean; isMocked?: boolean; + isKeyless?: boolean; passphraseState?: string; walletNo: number; walletOrderSaved?: number; // db field diff --git a/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts b/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts index 9e2297e13e51..e0ae6b53927e 100644 --- a/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts +++ b/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts @@ -379,6 +379,15 @@ class ServiceAccount extends ServiceBase { return localDb.getDevice(dbDeviceId); } + @backgroundMethod() + async isKeylessWalletExists(): Promise { + if (process.env.NODE_ENV !== 'production') { + await timerUtils.wait(1500); + } + const { wallets } = await this.getAllWallets(); + return wallets.some((wallet) => wallet.isKeyless); + } + async getAllWallets( params: { refillWalletInfo?: boolean; excludeKeylessWallet?: boolean } = {}, ) { diff --git a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx index f231d7eac96a..01267ef2001c 100644 --- a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx +++ b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx @@ -411,10 +411,46 @@ export function useKeylessWallet() { ], ); + const enableKeylessWalletV2 = useCallback(async () => { + if (enableKeylessWalletLoadingRef.current) { + return; + } + await errorToastUtils.withErrorAutoToast(async () => { + try { + enableKeylessWalletLoadingRef.current = true; + setEnableKeylessWalletLoading(true); + + const exists = + await backgroundApiProxy.serviceAccount.isKeylessWalletExists(); + if (exists) { + Dialog.show({ + title: 'Keyless Wallet', + description: + 'You already have a Keyless Wallet on this device. No need to create another one.', + showCancelButton: false, + onConfirmText: intl.formatMessage({ + id: ETranslations.global_got_it, + }), + }); + } else { + navigation.navigate(ERootRoutes.Onboarding, { + screen: EOnboardingV2Routes.OnboardingV2, + params: { + screen: EOnboardingPagesV2.OneKeyIDLogin, + }, + }); + } + } finally { + setEnableKeylessWalletLoading(false); + } + }); + }, [intl, navigation]); + return { ...methods, // TODO handleKeylessWalletClick enableKeylessWallet, + enableKeylessWalletV2, enableKeylessWalletLoading, }; } diff --git a/packages/kit/src/components/Password/components/PasswordSetup.tsx b/packages/kit/src/components/Password/components/PasswordSetup.tsx index 1dac1d6795ac..c7cd886cf099 100644 --- a/packages/kit/src/components/Password/components/PasswordSetup.tsx +++ b/packages/kit/src/components/Password/components/PasswordSetup.tsx @@ -114,7 +114,7 @@ const PasswordSetup = ({ setTimeout(() => { form.setFocus('confirmPassCode'); }, 150); - }, [form]); + }, [form, onStepChange]); const clearPasscodeTimeOut = useCallback(() => { setPassCodeConfirmClear(false); diff --git a/packages/kit/src/provider/Bootstrap.tsx b/packages/kit/src/provider/Bootstrap.tsx index 922bed006c73..f409626a3f40 100644 --- a/packages/kit/src/provider/Bootstrap.tsx +++ b/packages/kit/src/provider/Bootstrap.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { useCallback, useEffect, useRef } from 'react'; import { debounce, isEqual, noop, upperFirst } from 'lodash'; @@ -37,11 +38,12 @@ import performance from '@onekeyhq/shared/src/performance'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; import { EDiscoveryModalRoutes, - // eslint-disable-next-line @typescript-eslint/no-unused-vars EGalleryRoutes, EModalRoutes, EModalSettingRoutes, EMultiTabBrowserRoutes, + EOnboardingPagesV2, + EOnboardingV2Routes, ETabEarnRoutes, ETabRoutes, } from '@onekeyhq/shared/src/routes'; @@ -677,18 +679,19 @@ export function Bootstrap() { // screen: EOnboardingPages.ConnectWallet, // }); // ---------------------------------------------- - // navigation.navigate(ERootRoutes.Onboarding, { - // screen: EOnboardingV2Routes.OnboardingV2, - // params: { - // screen: EOnboardingPagesV2.AddExistingWallet, - // }, - // }); + navigation.navigate(ERootRoutes.Onboarding, { + screen: EOnboardingV2Routes.OnboardingV2, + params: { + // screen: EOnboardingPagesV2.AddExistingWallet, + screen: EOnboardingPagesV2.CreateOrImportWallet, + }, + }); // navigation.navigate(ETabRoutes.Developer, { // screen: EGalleryRoutes.ComponentKeylessWallet, // }); - navigation.navigate(ETabRoutes.Developer, { - screen: EGalleryRoutes.ComponentOneKeyID, - }); + // navigation.navigate(ETabRoutes.Developer, { + // screen: EGalleryRoutes.ComponentOneKeyID, + // }); }, 1000); return () => clearTimeout(timer); diff --git a/packages/kit/src/views/Onboardingv2/pages/CreateOrImportWallet.tsx b/packages/kit/src/views/Onboardingv2/pages/CreateOrImportWallet.tsx index 5279f542de67..6bf5834f17ef 100644 --- a/packages/kit/src/views/Onboardingv2/pages/CreateOrImportWallet.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/CreateOrImportWallet.tsx @@ -20,6 +20,7 @@ import { YStack, } from '@onekeyhq/components'; import { generateMnemonic } from '@onekeyhq/core/src/secret'; +import { EKeylessWalletEnableScene } from '@onekeyhq/shared/src/keylessWallet/keylessWalletConsts'; import { ETranslations } from '@onekeyhq/shared/src/locale'; import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; import type { IOnboardingParamListV2 } from '@onekeyhq/shared/src/routes'; @@ -146,8 +147,11 @@ function CreateOrImportWallet() { const [expanded, setExpanded] = useState(false); const [keylessExpanded, setKeylessExpanded] = useState(false); const isKeylessWalletEnabled = useKeylessWalletFeatureIsEnabled(); - const { enableKeylessWallet, enableKeylessWalletLoading } = - useKeylessWallet(); + const { + enableKeylessWallet, + enableKeylessWalletLoading, + enableKeylessWalletV2, + } = useKeylessWallet(); const walletKeys = ['metamask', 'okx', 'rainbow', 'tokenpocket'] as const; const navigation = useAppNavigation(); @@ -194,13 +198,19 @@ function CreateOrImportWallet() { defaultLogger.account.wallet.onboard({ onboardMethod: 'connectHWWallet' }); }; - const handleKeylessWalletClick = useCallback(() => { + const handleKeylessWalletClick = useCallback(async () => { // await enableKeylessWallet({ // fromScene: EKeylessWalletEnableScene.Onboarding, // }); + // navigation.push(EOnboardingPagesV2.OneKeyIDLogin); + await enableKeylessWalletV2(); + }, [enableKeylessWalletV2]); - navigation.push(EOnboardingPagesV2.OneKeyIDLogin); - }, [navigation]); + const handleKeylessWalletLegacyClick = useCallback(async () => { + await enableKeylessWallet({ + fromScene: EKeylessWalletEnableScene.Onboarding, + }); + }, [enableKeylessWallet]); return ( @@ -296,6 +306,11 @@ function CreateOrImportWallet() { ) : null} {/* keyless wallet */} + {isKeylessWalletEnabled ? ( + + ) : null} {isKeylessWalletEnabled ? ( @@ -431,6 +446,7 @@ function CreateOrImportWallet() { ) : null} + {/* create new wallet */} From c94929e31d5ebf3484ff4a616820ee06ba09633d Mon Sep 17 00:00:00 2001 From: Franco Date: Fri, 26 Dec 2025 15:20:36 +0800 Subject: [PATCH 26/66] i18n --- .../Onboardingv2/pages/ConfirmPinPage.tsx | 18 ++++--- .../pages/CreateOrImportWallet.tsx | 49 ++++++++++++------- .../Onboardingv2/pages/CreatePasscodePage.tsx | 2 +- .../Onboardingv2/pages/CreatePinPage.tsx | 33 +++++++++---- .../views/Onboardingv2/pages/GetStarted.tsx | 5 +- .../Onboardingv2/pages/NewPinCreatedPage.tsx | 15 ++++-- .../Onboardingv2/pages/OneKeyIDLoginPage.tsx | 11 ++++- .../Onboardingv2/pages/VerifyPinPage.tsx | 49 +++++++++++++------ .../src/views/Setting/pages/Tab/config.tsx | 2 +- .../shared/src/locale/enum/translations.ts | 34 ++++++++++++- packages/shared/src/locale/json/bn.json | 34 ++++++++++++- packages/shared/src/locale/json/de.json | 34 ++++++++++++- packages/shared/src/locale/json/en_US.json | 36 +++++++++++++- packages/shared/src/locale/json/es.json | 36 +++++++++++++- packages/shared/src/locale/json/fr_FR.json | 36 +++++++++++++- packages/shared/src/locale/json/hi_IN.json | 34 ++++++++++++- packages/shared/src/locale/json/id.json | 36 +++++++++++++- packages/shared/src/locale/json/it_IT.json | 36 +++++++++++++- packages/shared/src/locale/json/ja_JP.json | 34 ++++++++++++- packages/shared/src/locale/json/ko_KR.json | 34 ++++++++++++- packages/shared/src/locale/json/pt.json | 36 +++++++++++++- packages/shared/src/locale/json/pt_BR.json | 36 +++++++++++++- packages/shared/src/locale/json/ru.json | 34 ++++++++++++- packages/shared/src/locale/json/th_TH.json | 34 ++++++++++++- packages/shared/src/locale/json/uk_UA.json | 34 ++++++++++++- packages/shared/src/locale/json/vi.json | 34 ++++++++++++- packages/shared/src/locale/json/zh_CN.json | 34 ++++++++++++- packages/shared/src/locale/json/zh_HK.json | 34 ++++++++++++- packages/shared/src/locale/json/zh_TW.json | 34 ++++++++++++- 29 files changed, 795 insertions(+), 83 deletions(-) diff --git a/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx index 5898d473e51d..a3c4442ee82e 100644 --- a/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx @@ -1,7 +1,9 @@ import { useCallback, useState } from 'react'; import { useRoute } from '@react-navigation/core'; +import { useIntl } from 'react-intl'; +import { ETranslations } from '@onekeyhq/shared/src/locale/enum/translations'; import type { IOnboardingParamListV2 } from '@onekeyhq/shared/src/routes'; import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; @@ -17,7 +19,7 @@ function ConfirmPinPage() { RouteProp >(); const { pin: originalPin } = route.params; - + const intl = useIntl(); const [confirmPin, setConfirmPin] = useState(''); const [isValid, setIsValid] = useState(false); const [errorMessage, setErrorMessage] = useState(''); @@ -32,14 +34,16 @@ function ConfirmPinPage() { if (filteredText === originalPin) { setIsValid(true); } else { - setErrorMessage('Incorrect PIN. Please try again.'); + setErrorMessage( + intl.formatMessage({ id: ETranslations.incorrect_pin }), + ); setIsValid(false); } } else { setIsValid(false); } }, - [originalPin], + [originalPin, intl], ); const handleConfirm = useCallback(() => { @@ -48,10 +52,12 @@ function ConfirmPinPage() { return ( - Keyless wallet + + {intl.formatMessage({ + id: ETranslations.keyless_wallet, + })} + diff --git a/packages/kit/src/views/Onboardingv2/pages/NewPinCreatedPage.tsx b/packages/kit/src/views/Onboardingv2/pages/NewPinCreatedPage.tsx index 16928ebb41a2..0fac87511342 100644 --- a/packages/kit/src/views/Onboardingv2/pages/NewPinCreatedPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/NewPinCreatedPage.tsx @@ -1,13 +1,16 @@ import { useEffect } from 'react'; +import { useIntl } from 'react-intl'; + import { Button, Icon, Page, SizableText, YStack } from '@onekeyhq/components'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; import useAppNavigation from '../../../hooks/useAppNavigation'; import { OnboardingLayout } from '../components/OnboardingLayout'; function NewPinCreatedPage() { const navigation = useAppNavigation(); - + const intl = useIntl(); // close this page 5s later automatically useEffect(() => { const timer = setTimeout(() => { @@ -33,13 +36,17 @@ function NewPinCreatedPage() { > - New PIN Created + + {intl.formatMessage({ id: ETranslations.new_pin_created })} + - You can change your PIN at anytime through Settings. + {intl.formatMessage({ + id: ETranslations.new_pin_created_desc, + })} diff --git a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx index 923a475a16b2..b92cd4ed0f1a 100644 --- a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx @@ -1,5 +1,6 @@ import { useCallback, useState } from 'react'; +import { useIntl } from 'react-intl'; import { StyleSheet } from 'react-native'; import type { IIconProps, IKeyOfIcons } from '@onekeyhq/components'; @@ -12,6 +13,7 @@ import { Spinner, YStack, } from '@onekeyhq/components'; +import { ETranslations } from '@onekeyhq/shared/src/locale/enum/translations'; import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; import { ListItem } from '../../../components/ListItem'; @@ -98,6 +100,7 @@ function OptionItem({ function OneKeyIDLoginPage() { const navigation = useAppNavigation(); const [isLoggingIn, setIsLoggingIn] = useState(false); + const intl = useIntl(); const handleGoogleLogin = useCallback(() => { setIsLoggingIn(true); @@ -115,9 +118,13 @@ function OneKeyIDLoginPage() { - Select your email + + {intl.formatMessage({ id: ETranslations.select_your_email })} + - Add a wallet with your Google or Apple account + {intl.formatMessage({ + id: ETranslations.select_your_email_desc, + })} diff --git a/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx index b6260183149e..8a81269a666e 100644 --- a/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx @@ -1,7 +1,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useRoute } from '@react-navigation/core'; +import { useIntl } from 'react-intl'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; import type { IOnboardingParamListV2 } from '@onekeyhq/shared/src/routes'; import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; @@ -24,6 +26,7 @@ const COOLDOWN_BY_ATTEMPT: Record = { }; function VerifyPinPage() { + const intl = useIntl(); const navigation = useAppNavigation(); const route = useRoute>(); @@ -43,17 +46,19 @@ function VerifyPinPage() { const { title, description } = useMemo(() => { if (isSocialLogin) { return { - title: 'Enter your PIN', - description: - 'This email already has a wallet created. Please enter your PIN to login.', + title: intl.formatMessage({ id: ETranslations.enter_your_pin }), + description: intl.formatMessage({ + id: ETranslations.enter_your_pin_desc, + }), }; } return { - title: 'Remember your PIN?', - description: - "Just a friendly reminder to keep your PIN fresh in memory. We'll check in from time to time.", + title: intl.formatMessage({ id: ETranslations.remember_your_pin }), + description: intl.formatMessage({ + id: ETranslations.remember_your_pin_desc, + }), }; - }, [isSocialLogin]); + }, [isSocialLogin, intl]); // Clear cooldown timer on unmount useEffect(() => { @@ -137,10 +142,12 @@ function VerifyPinPage() { } } else { // Periodic verification: simple error message, no retry mechanism - setErrorMessage('Incorrect PIN. Please try again.'); + setErrorMessage( + intl.formatMessage({ id: ETranslations.incorrect_pin }), + ); } } - }, [attemptsRemaining, isSocialLogin, navigation, startCooldown]); + }, [attemptsRemaining, isSocialLogin, navigation, startCooldown, intl]); const handleForgotPin = useCallback(() => { if (isSocialLogin) { @@ -161,10 +168,20 @@ function VerifyPinPage() { attemptsRemaining < MAX_ATTEMPTS && attemptsRemaining > 0 ) { - const baseMessage = `Incorrect PIN entered. ${attemptsRemaining} attempts remaining.`; + const baseMessage = intl.formatMessage( + { + id: ETranslations.pin_attempts_remaining, + }, + { + attemptsRemaining, + }, + ); if (cooldownSeconds > 0) { - return `${baseMessage} Try again in ${formatCooldownTime( - cooldownSeconds, + return `${baseMessage} ${intl.formatMessage( + { id: ETranslations.pin_attempts_cooldown }, + { + seconds: formatCooldownTime(cooldownSeconds), + }, )}.`; } return baseMessage; @@ -176,8 +193,12 @@ function VerifyPinPage() { ISettingsConfig = () => { ? undefined : { icon: 'InputOutline', - title: 'Reset PIN', + title: intl.formatMessage({ id: ETranslations.reset_pin }), onPress: (navigation) => { navigation?.navigate(ERootRoutes.Onboarding, { screen: EOnboardingV2Routes.OnboardingV2, diff --git a/packages/shared/src/locale/enum/translations.ts b/packages/shared/src/locale/enum/translations.ts index d75b55033b31..a7e07cbd5590 100644 --- a/packages/shared/src/locale/enum/translations.ts +++ b/packages/shared/src/locale/enum/translations.ts @@ -245,6 +245,7 @@ bandwidth_energy_rent_energy = 'bandwidth_energy.rent_energy', bandwidth_energy_title = 'bandwidth_energy.title', bandwidth_energy_what_is_bandwidth_energy = 'bandwidth_energy.what_is_bandwidth_energy', + beginner_friendly = 'beginner_friendly', bip44__standard = 'bip44__standard', bluetooth_disable_in_settings = 'bluetooth.disable_in_settings', bluetooth_disabled = 'bluetooth.disabled', @@ -299,6 +300,8 @@ communication_timeout = 'communication.timeout', confirm_exit_dialog_desc = 'confirm_exit_dialog_desc', confirm_exit_dialog_title = 'confirm_exit_dialog_title', + confirm_your_pin = 'confirm_your_pin', + confirm_your_pin_desc = 'confirm_your_pin_desc', connect_device_to_computer_via_usb = 'connect_device_to_computer_via_usb', connect_with_qr_code = 'connect_with_qr_code', contact_us_instruction = 'contact_us_instruction', @@ -311,6 +314,7 @@ content__normal = 'content__normal', content__slow = 'content__slow', content__to = 'content__to', + continue_with_social_platform = 'continue_with_social_platform', copy_address_modal_item_create_address_instruction = 'copy_address_modal_item_create_address_instruction', copy_address_modal_title = 'copy_address_modal_title', copy_anyway = 'copy_anyway', @@ -319,6 +323,8 @@ count_assets = 'count_assets', count_hidden_assets = 'count_hidden_assets', count_words = 'count_words', + create_a_pin = 'create_a_pin', + create_a_pin_desc = 'create_a_pin_desc', create_new_wallet_badge_consists = 'create_new_wallet_badge_consists', create_new_wallet_badge_handwritten = 'create_new_wallet_badge_handwritten', create_new_wallet_badge_keep = 'create_new_wallet_badge_keep', @@ -326,6 +332,7 @@ create_new_wallet_badge_most_used = 'create_new_wallet_badge_most_used', create_new_wallet_badge_supports = 'create_new_wallet_badge_supports', create_new_wallet_learn_more = 'create_new_wallet_learn_more', + create_passcode_desc = 'create_passcode_desc', create_qr_based_hidden_wallet_create_hidden_wallet_desc = 'create_qr_based_hidden_wallet_create_hidden_wallet_desc', create_qr_based_hidden_wallet_create_hidden_wallet_title = 'create_qr_based_hidden_wallet_create_hidden_wallet_title', create_qr_based_hidden_wallet_create_standard_wallet_desc = 'create_qr_based_hidden_wallet_create_standard_wallet_desc', @@ -871,6 +878,8 @@ enter_pin_invalid_pin = 'enter_pin.invalid_pin', enter_pin_title = 'enter_pin.title', enter_pin_on_app = 'enter_pin_on_app', + enter_your_pin = 'enter_your_pin', + enter_your_pin_desc = 'enter_your_pin_desc', explore_add_bookmark = 'explore.add_bookmark', explore_add_to_whitelist = 'explore.add_to_whitelist', explore_addresses_count = 'explore.addresses_count', @@ -1077,6 +1086,7 @@ for_large_assets = 'for_large_assets', for_reference_only = 'for_reference_only', forgot_password_no_question_mark = 'forgot_password_no_question_mark', + forgot_pin = 'forgot_pin', form_address_error_invalid = 'form.address_error_invalid', form_address_placeholder = 'form.address_placeholder', form_amount_placeholder = 'form.amount_placeholder', @@ -1372,7 +1382,7 @@ global_faqs_bridge_download = 'global.faqs_bridge_download', global_faqs_firmware_detection = 'global.faqs_firmware_detection', global_faqs_forgot_pin = 'global.faqs_forgot_pin', - global_faqs_reset_wallet = 'global.faqs_reset_wallet', + global_faqs_reset_device = 'global.faqs_reset_device', global_favorites = 'global.favorites', global_fcc_id = 'global.fcc_id', global_fdv = 'global.fdv', @@ -1679,8 +1689,10 @@ global_search_account_selector = 'global.search_account_selector', global_search_address = 'global.search_address', global_search_asset = 'global.search_asset', + global_search_everything = 'global.search_everything', global_search_no_results_desc = 'global.search_no_results_desc', global_search_no_results_title = 'global.search_no_results_title', + global_search_placeholder_web = 'global.search_placeholder_web', global_search_tokens = 'global.search_tokens', global_secure_install = 'global.secure_install', global_security = 'global.security', @@ -1819,6 +1831,7 @@ global_watch_only_wallet = 'global.watch_only_wallet', global_watched = 'global.watched', global_watchlist = 'global.watchlist', + global_web_access_for_hardware_wallet_disconnected = 'global.web_access_for_hardware_wallet_disconnected', global_web_feature_not_available_go_to_app = 'global.web_feature_not_available_go_to_app', global_website = 'global.website', global_what_happen = 'global.what_happen', @@ -1964,9 +1977,12 @@ import_backup_password_desc = 'import_backup_password_desc', import_hardware_phrases_warning = 'import_hardware_phrases_warning', import_phrase_or_private_key = 'import_phrase_or_private_key', + incorrect_pin = 'incorrect_pin', insufficient_fee_append_desc = 'insufficient_fee_append_desc', interact_with_contract = 'interact_with_contract', kaspa_official = 'kaspa_official', + keyless_wallet = 'keyless_wallet', + keyless_wallet_desc = 'keyless_wallet_desc', learn_how_to_withdraw_crypto_from_exchange = 'learn_how_to_withdraw_crypto_from_exchange', learn_more_about_qr_code_wallet = 'learn_more_about_qr_code_wallet', lightning_invoice = 'lightning_invoice', @@ -2049,6 +2065,7 @@ market_ath_desc = 'market.ath_desc', market_atl_desc = 'market.atl_desc', market_cex = 'market.cex', + market_change_24h = 'market.change_24h', market_chart = 'market.chart', market_days_since_launch = 'market.days_since_launch', market_empty_watchlist_desc = 'market.empty_watchlist_desc', @@ -2152,6 +2169,8 @@ network_selector_unavailable_networks = 'network_selector.unavailable_networks', network_show_enabled_only = 'network_show_enabled_only', network_visible_in_all_network_tooltip_title = 'network_visible_in_all_network_tooltip_title', + new_pin_created = 'new_pin_created', + new_pin_created_desc = 'new_pin_created_desc', nft_already_collected = 'nft.already_collected', nft_attributes = 'nft.attributes', nft_collect_failed = 'nft.collect_failed', @@ -2285,6 +2304,7 @@ open_as_sidebar = 'open_as_sidebar', open_in_mobile_app = 'open_in_mobile_app', open_ordinals_transfer_tutorial_url_message = 'open_ordinals_transfer_tutorial_url_message', + open_source_secure_sharding = 'open_source_secure_sharding', p2pkh_desc = 'p2pkh_desc', p2sh_p2wpkh_desc = 'p2sh_p2wpkh_desc', p2tr_desc = 'p2tr_desc', @@ -2569,6 +2589,7 @@ perps_buy_tip = 'perps.buy_tip', perps_get_reward = 'perps.get_reward', perps_offline_moblie = 'perps.offline_moblie', + perps_settings_return_to_default_layout = 'perps.settings_return_to_default_layout', perps_share_position_background = 'perps.share_position_background', perps_share_position_btn_Share_on_x = 'perps.share_position_btn_Share_on_x', perps_share_position_btn_copy_link = 'perps.share_position_btn_copy_link', @@ -2579,6 +2600,8 @@ perps_token_selector_stocks = 'perps.token_selector_stocks', perps_trade_reward = 'perps.trade_reward', pick_your_device = 'pick_your_device', + pin_attempts_cooldown = 'pin_attempts_cooldown', + pin_attempts_remaining = 'pin_attempts_remaining', preparing_backup_desc = 'preparing_backup_desc', preparing_backup_title = 'preparing_backup_title', prime_about_cloud_sync = 'prime.about_cloud_sync', @@ -2831,6 +2854,7 @@ receive_from_another_wallet_desc = 'receive_from_another_wallet_desc', receive_from_exchange = 'receive_from_exchange', receive_token_list_footer_text = 'receive_token_list_footer_text', + recovery_phrase_free = 'recovery_phrase_free', recovery_phrase_screenshot_protected_desc = 'recovery_phrase_screenshot_protected_desc', recovery_phrase_screenshot_protected_title = 'recovery_phrase_screenshot_protected_title', referral_accept = 'referral.accept', @@ -3020,6 +3044,8 @@ referral_your_referred_wallets_more_address = 'referral.your_referred_wallets_more_address', referral_code_tutorial_label = 'referral_code_tutorial_label', referral_promo_title = 'referral_promo_title', + remember_your_pin = 'remember_your_pin', + remember_your_pin_desc = 'remember_your_pin_desc', remove_account_desc = 'remove_account_desc', remove_device = 'remove_device', remove_device_desc = 'remove_device_desc', @@ -3032,6 +3058,7 @@ remove_wallet_desc = 'remove_wallet_desc', remove_wallet_double_confirm_message = 'remove_wallet_double_confirm_message', reset_app_desc = 'reset_app_desc', + reset_pin = 'reset_pin', scan_camera_access_denied = 'scan.camera_access_denied', scan_enable_camera_permissions = 'scan.enable_camera_permissions', scan_grant_camera_access_in_expand_view = 'scan.grant_camera_access_in_expand_view', @@ -3049,9 +3076,12 @@ scan_to_create_an_address = 'scan_to_create_an_address', scanning_text = 'scanning_text', secure_qr_toast_scan_qr_code_on_device_text = 'secure_qr_toast_scan_qr_code_on_device_text', + seed_phrase_wallet = 'seed_phrase_wallet', select_connect_app_on_home = 'select_connect_app_on_home', select_onekey_app = 'select_onekey_app', select_recovery_phrase_length = 'select_recovery_phrase_length', + select_your_email = 'select_your_email', + select_your_email_desc = 'select_your_email_desc', selected_network = 'selected_network', selected_network_only_supports_device = 'selected_network_only_supports_device', self_troubleshooting = 'self_troubleshooting', @@ -3371,6 +3401,7 @@ swap_btn_approving = 'swap.btn_approving', swap_btn_building = 'swap.btn_building', swap_ch_status_hold = 'swap.ch_status_hold', + swap_current_token = 'swap.current_token', swap_limit_amount = 'swap.limit_amount', swap_loading_content = 'swap.loading_content', swap_native_token_max_tip = 'swap.native_token_max_tip', @@ -3740,6 +3771,7 @@ tx_accelerate_speed_up_with_accelerator_dialog_note_service_provide_by = 'tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by', tx_accelerate_speed_up_with_accelerator_dialog_note_title = 'tx_accelerate.speed_up_with_accelerator_dialog_note_title', tx_accelerate_speed_up_with_accelerator_dialog_title = 'tx_accelerate.speed_up_with_accelerator_dialog_title', + ultra_fast_setup = 'ultra_fast_setup', update_all_other_apps_closed = 'update.all_other_apps_closed', update_all_other_apps_closed_emoji = 'update.all_other_apps_closed_emoji', update_all_updates_complete = 'update.all_updates_complete', diff --git a/packages/shared/src/locale/json/bn.json b/packages/shared/src/locale/json/bn.json index 0b5ae287a90d..32f7171a4095 100644 --- a/packages/shared/src/locale/json/bn.json +++ b/packages/shared/src/locale/json/bn.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "ভাড়া এনার্জি", "bandwidth_energy.title": "ব্যান্ডউইথ এবং এনার্জি", "bandwidth_energy.what_is_bandwidth_energy": "ট্রনে ব্যান্ডউইথ এবং এনার্জি কি?", + "beginner_friendly": "সহজে ব্যবহারযোগ্য নবাগত-বান্ধব", "bip44__standard": "BIP44 মানদণ্ড", "bluetooth.disable_in_settings": "আপনি যদি শুধুমাত্র USB ব্যবহার করতে পছন্দ করেন এবং এই বার্তাটি আর দেখতে না চান, তাহলে OneKey অ্যাপ > সেটিংস > প্রেফারেন্সেস-এ যান এবং ব্লুটুথ বন্ধ করুন।", "bluetooth.disabled": "ব্লুটুথ অক্ষম করা হয়েছে", @@ -294,6 +295,8 @@ "communication.timeout": "যোগাযোগের সময়সীমা শেষ", "confirm_exit_dialog_desc": "আপনি কি নিশ্চিত যে আপনি ডাটা মাইগ্রেশন প্রক্রিয়া থেকে বের হতে চান?", "confirm_exit_dialog_title": "নিশ্চিত করুন বের হওয়া", + "confirm_your_pin": "আপনার পিন নিশ্চিত করুন", + "confirm_your_pin_desc": "আপনি যদি এই পিনটি ভুলে যান, তবে নতুন ডিভাইসে আপনার ওয়ালেট পুনরুদ্ধার করতে পারবেন না।", "connect_device_to_computer_via_usb": "আপনার কম্পিউটারের সাথে USB এর মাধ্যমে {deviceLabel} সংযুক্ত করুন", "connect_with_qr_code": "QR কোড দিয়ে সংযোগ করুন", "contact_us_instruction": "আরো সাহায্য প্রয়োজন?", @@ -306,6 +309,7 @@ "content__normal": "নরমাল", "content__slow": "ধীর", "content__to": "প্রতি", + "continue_with_social_platform": "{platform} দিয়ে চালিয়ে যান", "copy_address_modal_item_create_address_instruction": "ঠিকানা তৈরি করুন", "copy_address_modal_title": "অ্যাকাউন্ট ঠিকানা", "copy_anyway": "তবুও অনুলিপি করুন", @@ -314,6 +318,8 @@ "count_assets": "{count} সম্পদ", "count_hidden_assets": "{count} গোপন সম্পদ", "count_words": "{length} শব্দ", + "create_a_pin": "একটি পিন তৈরি করুন", + "create_a_pin_desc": "এটি আপনার সব ডিভাইসে আপনার ওয়ালেট সুরক্ষিত রাখতে ব্যবহৃত হয়। এটি পুনরুদ্ধার করা যাবে না.", "create_new_wallet_badge_consists": "রিকভারি ফ্রেজে ১২টি শব্দ থাকে", "create_new_wallet_badge_handwritten": "হাতে লেখা ব্যাকআপ", "create_new_wallet_badge_keep": "নিজেকেই নিরাপদে রাখতে হবে", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "সবচেয়ে বেশি ব্যবহৃত", "create_new_wallet_badge_supports": "শত শত নেটওয়ার্ক সমর্থন করে", "create_new_wallet_learn_more": "রিকভারি ফ্রেজটি আপনার ওয়ালেটের নিরাপত্তার মূল ভিত্তি। এটি ১২টি সাধারণ ইংরেজি শব্দ নিয়ে গঠিত, যা আপনার প্রাইভেট কী এবং ওয়ালেট ঠিকানা তৈরি ও পুনরুদ্ধারে ব্যবহৃত হয়। এটি হাতে লিখে রাখুন এবং নিরাপদে সংরক্ষণ করুন — আপনার সম্পদে কেবল আপনারই প্রবেশাধিকার রয়েছে।", + "create_passcode_desc": "আপনি এটি আপনার ওয়ালেট আনলক করতে ব্যবহার করবেন।", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "স্ট্যান্ডার্ড ওয়ালেট তৈরি হওয়ার পর, একটি লুকানো ওয়ালেট তৈরি করতে একটি passphrase প্রবেশ করান।", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "গোপন ওয়ালেট তৈরি করুন", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "QR কোড প্রদর্শন করার আগে কোনো passphrase প্রয়োজন নেই। ✅ বোতামটি টিপুন, কোডটি দেখান এবং অ্যাপটির সাথে স্ক্যান করে একটি QR-based স্ট্যান্ডার্ড ওয়ালেট তৈরি করুন।", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "অবৈধ পিন কোড", "enter_pin.title": "পিন কোড প্রবেশ করান", "enter_pin_on_app": "অ্যাপে পিন প্রবেশ করান", + "enter_your_pin": "আপনার পিন লিখুন", + "enter_your_pin_desc": "এই ইমেইলের জন্য ইতিমধ্যেই একটি ওয়ালেট তৈরি করা হয়েছে। লগইন করতে আপনার পিন লিখুন।", "explore.add_bookmark": "বুকমার্কে সংযুক্তকরন", "explore.add_to_whitelist": "হোয়াইটলিস্টে যোগ করুন", "explore.addresses_count": "{number} ঠিকানা", @@ -1072,6 +1081,7 @@ "for_large_assets": "বড় সম্পদের জন্য", "for_reference_only": "শুধুমাত্র রেফারেন্সের জন্য", "forgot_password_no_question_mark": "পাসওয়ার্ড ভুলে গেছেন", + "forgot_pin": "PIN ভুলে গেছেন?", "form.address_error_invalid": "অবৈধ ঠিকানা", "form.address_placeholder": "ঠিকানা বা ডোমেইন", "form.amount_placeholder": "পরিমাণ প্রবেশ করুন", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "ব্রিজ ডাউনলোড", "global.faqs_firmware_detection": "সংযোগ পরীক্ষা", "global.faqs_forgot_pin": "PIN ভুলে গেছেন", - "global.faqs_reset_wallet": "ওয়ালেট রিসেট", + "global.faqs_reset_device": "ডিভাইস রিসেট করুন", "global.favorites": "প্রিয়", "global.fcc_id": "FCC আইডি", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "অ্যাকাউন্টের নাম অনুসন্ধান করুন", "global.search_address": "ঠিকানা অনুসন্ধান করুন", "global.search_asset": "সম্পদ অনুসন্ধান করুন", + "global.search_everything": "সব কিছু খুঁজুন", "global.search_no_results_desc": "অনুসন্ধানের কীওয়ার্ডটি পরিবর্তন করার চেষ্টা করুন", "global.search_no_results_title": "কোন ফলাফল নেই", + "global.search_placeholder_web": "অনুসন্ধান", "global.search_tokens": "অনুসন্ধান টোকেন", "global.secure_install": "নিরাপদ ইনস্টল", "global.security": "নিরাপত্তা", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "শুধুমাত্র দেখার জন্য ওয়ালেট", "global.watched": "দেখা হয়েছে", "global.watchlist": "নজরতালিকা", + "global.web_access_for_hardware_wallet_disconnected": "হার্ডওয়ালেট সংযোগের জন্য ওয়েব এক্সেস বন্ধ করে দেওয়া হয়েছে। সংযুক্ত ডিভাইসগুলো এখনও ব্যবহার করা যাবে; যদি কোনো সমস্যার মুখোমুখি হন, দয়া করে App অথবা ব্রাউজার এক্সটেনশন ব্যবহার করুন।", "global.web_feature_not_available_go_to_app": "এই বৈশিষ্ট্যটি ওয়েবে উপলব্ধ নেই। অনুগ্রহ করে ডেস্কটপ বা মোবাইল অ্যাপ ব্যবহার করুন।", "global.website": "ওয়েবসাইট", "global.what_happen": "কি হবে:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "অনুগ্রহ করে এই ব্যাকআপের জন্য পাসওয়ার্ড লিখুন।", "import_hardware_phrases_warning": "আপনার হার্ডওয়্যার ওয়ালেটের রিকভারি ফ্রেজ ইম্পোর্ট করবেন না। হার্ডওয়্যার ওয়ালেট সংযুক্ত করুন ↗ বরং করুন", "import_phrase_or_private_key": "ফ্রেজ বা প্রাইভেট কী ইমপোর্ট করুন", + "incorrect_pin": "ভুল পিন। অনুগ্রহ করে আবার চেষ্টা করুন।", "insufficient_fee_append_desc": "সর্বাধিক আনুমানিক ফি ভিত্তিক: {amount} {symbol}", "interact_with_contract": "(এর সাথে) যোগাযোগ করুন (প্রতি)", "kaspa_official": "Kaspa অফিসিয়াল", + "keyless_wallet": "কীলেস ওয়ালেট", + "keyless_wallet_desc": "আপনার কী-লেস ওয়ালেট একাধিক ফ্যাক্টরে নিরাপদভাবে এবং বিকেন্দ্রীভূতভাবে সংরক্ষণ করা হয়। আপনার Google বা Apple অ্যাকাউন্ট এবং একটি ৪-সংখ্যার পিন ব্যবহার করে সহজেই ওয়ালেটের এক্সেস পুনরুদ্ধার করুন।", "learn_how_to_withdraw_crypto_from_exchange": "এই এক্সচেঞ্জগুলো থেকে ক্রিপ্টো সম্পদ OneKey-এ কীভাবে উত্তোলন করবেন তা জানুন", "learn_more_about_qr_code_wallet": "QR-based ওয়ালেট সম্পর্কে আরও জানুন", "lightning_invoice": "লাইটনিং ইনভয়েস", @@ -2044,6 +2060,7 @@ "market.ath_desc": "{token} এর সর্বকালের সর্বোচ্চ মূল্য ছিল {time} তারিখে, {price} এ, এবং বর্তমান মূল্য সেই সর্বোচ্চ মূল্য থেকে {percent} কম।", "market.atl_desc": "{token} এর সর্বকালের সর্বনিম্ন মূল্য ছিল {time} , {price} এ, এবং বর্তমান মূল্য সেই নিম্ন থেকে {percent} বেড়েছে৷", "market.cex": "CEX", + "market.change_24h": "পরিবর্তন / 24h", "market.chart": "চার্ট", "market.days_since_launch": "লঞ্চের পর দিন", "market.empty_watchlist_desc": "আপনার প্রিয় টোকেনগুলি ওয়াচলিস্টে যোগ করুন", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "নির্বাচিত অ্যাকাউন্টের জন্য অনুপলব্ধ নেটওয়ার্ক", "network_show_enabled_only": "শুধুমাত্র সক্রিয় নেটওয়ার্কগুলি দেখান", "network_visible_in_all_network_tooltip_title": "'সব নেটওয়ার্ক' দৃশ্যে প্রদর্শিত", + "new_pin_created": "নতুন পিন তৈরি হয়েছে", + "new_pin_created_desc": "আপনি যেকোনো সময় সেটিংস থেকে আপনার পিন পরিবর্তন করতে পারেন।", "nft.already_collected": "এই NFT ইতিমধ্যে সংগ্রহ করা হয়েছে।", "nft.attributes": "বৈশিষ্ট্যাবলী", "nft.collect_failed": "NFT সংগ্রহ ব্যর্থ হয়েছে, দয়া করে আবার চেষ্টা করুন।", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "সাইড প্যানেল হিসাবে খুলুন", "open_in_mobile_app": "মোবাইল অ্যাপে খুলুন", "open_ordinals_transfer_tutorial_url_message": "Ordinals সম্পদ স্থানান্তর করার নির্দেশনা কি?", + "open_source_secure_sharding": "ওপেন সোর্স নিরাপদ শেডিং", "p2pkh_desc": "\"1\" দিয়ে শুরু হয়। P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "\"bc1q\" দিয়ে শুরু হয়। P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "\"bc1p\" দিয়ে শুরু হয়। P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "আরও দরকার {token} ?", "perps.get_reward": "পুরষ্কার পান", "perps.offline_moblie": "সংযোগ বিচ্ছিন্ন হয়েছে। অনুগ্রহ করে আপনার নেটওয়ার্ক পরীক্ষা করে পুল-টু-রিফ্রেশ করে আবার চেষ্টা করুন।", + "perps.settings_return_to_default_layout": "ডিফল্ট বিন্যাসে ফিরে যান", "perps.share_position_background": "পটভূমি", "perps.share_position_btn_Share_on_x": "X-এ শেয়ার করুন", "perps.share_position_btn_copy_link": "লিংক কপি করুন", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "স্টক", "perps.trade_reward": "ট্রেড পুরষ্কার", "pick_your_device": "আপনার ডিভাইস নির্বাচন করুন", + "pin_attempts_cooldown": "{seconds} পরে আবার চেষ্টা করুন।", + "pin_attempts_remaining": "ভুল পিন প্রবেশ করা হয়েছে। ${attemptsRemaining}টি চেষ্টা বাকি।", "preparing_backup_desc": "একটু অপেক্ষা করুন…", "preparing_backup_title": "ব্যাকআপ প্রস্তুত করা হচ্ছে…", "prime.about_cloud_sync": "ক্লাউড সিঙ্ক সম্বন্ধে", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "আপনার ওয়ালেট ঠিকানা ব্যবহার করে গ্রহণ করুন", "receive_from_exchange": "বিনিময় থেকে গ্রহণ করুন", "receive_token_list_footer_text": "টোকেন খুঁজে পাচ্ছেন না? খোঁজার চেষ্টা করুন", + "recovery_phrase_free": "রিকভারি ফ্রেজ-মুক্ত", "recovery_phrase_screenshot_protected_desc": "আপনার সম্পদের নিরাপত্তার জন্য, আপনার পুনরুদ্ধার বাক্যাংশটি কখনোই স্ক্রিনশটে দেখা যাবে না।", "recovery_phrase_screenshot_protected_title": "রিকভারি ফ্রেজ সুরক্ষিত", "referral.accept": "গ্রহণ করুন", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} ঠিকানা", "referral_code_tutorial_label": "রেফারাল কোড কীভাবে পাবো?", "referral_promo_title": "OneKey রেফারেল প্রোগ্রামে যোগ দিন", + "remember_your_pin": "আপনি কি আপনার PIN মনে রেখেছেন?", + "remember_your_pin_desc": "শুধু বন্ধুত্বপূর্ণভাবে মনে করিয়ে দিচ্ছি, আপনার পিন যেন সবসময় মনে থাকে। আমরা সময়ে সময়ে এটি যাচাই করব।", "remove_account_desc": "এই অ্যাকাউন্টটি মুছে ফেলা হবে।", "remove_device": "ডিভাইস মুছুন", "remove_device_desc": "এটি OneKey App থেকে এই হার্ডওয়্যার ওয়ালেটটি সংযোগ বিচ্ছিন্ন করবে। আপনি যখনই চান তখন এটি App এ পুনরায় সংযোগ করতে পারেন।", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "আপনি যেন ওয়ালেট সরানোর আগে রিকভারি ফ্রেজটি লিখে রাখেন তা নিশ্চিত করুন। নতুবা, আপনি ওয়ালেটটি পুনরুদ্ধার করতে পারবেন না।", "remove_wallet_double_confirm_message": "আমি পুনরুদ্ধার বাক্যাংশটি লিখে রেখেছি", "reset_app_desc": "এটি OneKey এ আপনার তৈরি সমস্ত ডেটা মুছে ফেলবে। আপনার যথাযথ ব্যাকআপ রয়েছে তা নিশ্চিত হওয়ার পরে, অ্যাপটি রিসেট করতে \"RESET\" লিখুন", + "reset_pin": "পিন রিসেট করুন", "scan.camera_access_denied": "ক্যামেরা অ্যাক্সেস অস্বীকার করা হয়েছে", "scan.enable_camera_permissions": "OneKey কিউআর কোড স্ক্যান করতে ক্যামেরা অ্যাক্সেসের প্রয়োজন। দয়া করে \"সেটিংস\" এ যান এবং এই বৈশিষ্ট্যটি ব্যবহার করতে ক্যামেরা অনুমতিগুলি সক্রিয় করুন।", "scan.grant_camera_access_in_expand_view": "দয়া করে এক্সপ্যান্ড ভিউতে ক্যামেরা অ্যাক্সেস অনুমোদন করুন।", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "ঠিকানা তৈরি করতে স্ক্যান করুন", "scanning_text": "স্ক্যানিং", "secure_qr_toast_scan_qr_code_on_device_text": "QR কোড দেখা যাবার পর ফিরে আসুন, 'নেক্সট' ক্লিক করুন, তারপর এটি স্ক্যান করুন।", + "seed_phrase_wallet": "সীড ফ্রেজ ওয়ালেট", "select_connect_app_on_home": "হোম স্ক্রিনে Connect App Wallet নির্বাচন করুন", "select_onekey_app": "OneKey App নির্বাচন করুন", "select_recovery_phrase_length": "একটি দৈর্ঘ্য নির্বাচন করুন", + "select_your_email": "আপনার ইমেইল নির্বাচন করুন", + "select_your_email_desc": "আপনার Google বা Apple অ্যাকাউন্ট দিয়ে একটি ওয়ালেট যোগ করুন", "selected_network": "নির্বাচিত নেটওয়ার্ক", "selected_network_only_supports_device": "নির্বাচিত নেটওয়ার্ক বর্তমানে কেবল {deviceType} সমর্থন করে", "self_troubleshooting": "স্বয়ংক্রিয় সমস্যা সমাধান", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "অনুমোদন করা হচ্ছে...", "swap.btn_building": "অর্ডার তৈরি হচ্ছে...", "swap.ch_status_hold": "সহায়তার সাথে যোগাযোগ করুন", + "swap.current_token": "বর্তমান টোকেন", "swap.limit_amount": "{num1} {fromToken} বিক্রি করে {num2} {toToken} নিন", "swap.loading_content": "সেরা মূল্য গণনা করা হচ্ছে...", "swap.native_token_max_tip": "অনুগ্রহ করে নেটওয়ার্ক ফি পরিশোধের জন্য কিছু পরিমাণ টোকেন সংরক্ষণ করুন, অন্যথায় লেনদেনটি ব্যর্থ হবে।", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "সেবা প্রদান করা হয়েছে {accelerator} দ্বারা", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "দ্রষ্টব্য", "tx_accelerate.speed_up_with_accelerator_dialog_title": "{accelerator} দিয়ে গতি বাড়ান", + "ultra_fast_setup": "অতি দ্রুত সেটআপ", "update.all_other_apps_closed": "অন্য সমস্ত OneKey অ্যাপ এবং ওয়েব আপগ্রেড সরঞ্জামগুলি বন্ধ আছে।", "update.all_other_apps_closed_emoji": "অন্য সমস্ত OneKey অ্যাপ এবং ওয়েব আপগ্রেড সরঞ্জাম বন্ধ করা হয়েছে। 🆗", "update.all_updates_complete": "সমস্ত আপডেট সম্পন্ন হয়েছে 👏🏻", diff --git a/packages/shared/src/locale/json/de.json b/packages/shared/src/locale/json/de.json index a5998d473784..98a54595418e 100644 --- a/packages/shared/src/locale/json/de.json +++ b/packages/shared/src/locale/json/de.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "Energie mieten", "bandwidth_energy.title": "Bandbreite & Energie", "bandwidth_energy.what_is_bandwidth_energy": "Was sind Bandbreite & Energie bei Tron?", + "beginner_friendly": "Einsteigerfreundlich", "bip44__standard": "BIP44 Standard", "bluetooth.disable_in_settings": "Wenn Sie ausschließlich USB verwenden möchten und diese Aufforderung nicht erneut sehen wollen, gehen Sie in der OneKey-App zu Einstellungen > Präferenzen und schalten Sie Bluetooth aus.", "bluetooth.disabled": "Bluetooth ist deaktiviert", @@ -294,6 +295,8 @@ "communication.timeout": "Kommunikationszeitüberschreitung", "confirm_exit_dialog_desc": "Sind Sie sicher, dass Sie den Datenmigrationsprozess beenden möchten?", "confirm_exit_dialog_title": "Bestätigen Sie den Ausgang", + "confirm_your_pin": "Bestätigen Sie Ihre PIN", + "confirm_your_pin_desc": "Wenn du diese PIN vergisst, kannst du deine Wallet auf einem neuen Gerät nicht wiederherstellen.", "connect_device_to_computer_via_usb": "Verbinde {deviceLabel} über USB mit deinem Computer", "connect_with_qr_code": "Mit QR-Code verbinden", "contact_us_instruction": "Benötigen Sie weitere Hilfe?", @@ -306,6 +309,7 @@ "content__normal": "Normal", "content__slow": "Langsam", "content__to": "Zu", + "continue_with_social_platform": "Weiter mit {platform}", "copy_address_modal_item_create_address_instruction": "Adresse erstellen", "copy_address_modal_title": "Kontoadresse", "copy_anyway": "Trotzdem kopieren", @@ -314,6 +318,8 @@ "count_assets": "{count} Vermögenswerte", "count_hidden_assets": "{count} versteckte Vermögenswerte", "count_words": "{length} Wörter", + "create_a_pin": "PIN erstellen", + "create_a_pin_desc": "Dies wird verwendet, um deine Wallet auf all deinen Geräten zu sichern. Dies kann nicht wiederhergestellt werden.", "create_new_wallet_badge_consists": "Wiederherstellungsphrase besteht aus 12 Wörtern", "create_new_wallet_badge_handwritten": "Handschriftliche Sicherungskopie", "create_new_wallet_badge_keep": "Du musst selbst darauf achten, es sicher aufzubewahren", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "Am häufigsten verwendet", "create_new_wallet_badge_supports": "Unterstützt Hunderte von Netzwerken", "create_new_wallet_learn_more": "Die Wiederherstellungsphrase ist das Herzstück der Sicherheit deiner Wallet. Sie besteht aus 12 gängigen englischen Wörtern, mit denen dein privater Schlüssel und deine Wallet-Adresse erstellt und wiederhergestellt werden. Schreibe sie von Hand auf und bewahre sie sicher auf — nur du hast Zugriff auf deine Vermögenswerte.", + "create_passcode_desc": "Sie verwenden dies, um Ihre Wallet zu entsperren.", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "Nachdem die Standard-Wallet erstellt wurde, geben Sie eine Passphrase ein, um eine versteckte Wallet zu erstellen.", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "Versteckte Geldbörse erstellen", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "Es wird keine Passphrase benötigt, bevor der QR-Code angezeigt wird. Tippen Sie auf die ✅ Taste, zeigen Sie den Code und scannen Sie ihn mit der App, um eine QR-basierte Standard-Wallet zu erstellen.", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "Ungültiger PIN-Code", "enter_pin.title": "Geben Sie den PIN-Code ein", "enter_pin_on_app": "Geben Sie die PIN in der App ein", + "enter_your_pin": "Geben Sie Ihre PIN ein", + "enter_your_pin_desc": "Für diese E-Mail-Adresse wurde bereits eine Wallet erstellt. Bitte gib deine PIN ein, um dich anzumelden.", "explore.add_bookmark": "Lesezeichen hinzufügen", "explore.add_to_whitelist": "Zur Whitelist hinzufügen", "explore.addresses_count": "{number} Adressen", @@ -1072,6 +1081,7 @@ "for_large_assets": "Für große Vermögenswerte", "for_reference_only": "Nur zur Referenz", "forgot_password_no_question_mark": "Passwort vergessen", + "forgot_pin": "PIN vergessen?", "form.address_error_invalid": "Ungültige Adresse", "form.address_placeholder": "Adresse oder Domain", "form.amount_placeholder": "Geben Sie den Betrag ein", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "Hardware-Bridge herunterladen", "global.faqs_firmware_detection": "Verbindungstest", "global.faqs_forgot_pin": "PIN vergessen", - "global.faqs_reset_wallet": "Wallet zurücksetzen", + "global.faqs_reset_device": "Gerät zurücksetzen", "global.favorites": "Favoriten", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "Kontoname suchen", "global.search_address": "Adresse suchen", "global.search_asset": "Vermögenswert suchen", + "global.search_everything": "Alles durchsuchen", "global.search_no_results_desc": "Versuchen Sie, das Suchschlüsselwort zu ändern", "global.search_no_results_title": "Keine Ergebnisse", + "global.search_placeholder_web": "Suche", "global.search_tokens": "Suchtoken", "global.secure_install": "Sichere Installation", "global.security": "Sicherheit", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "Watch-Only Wallet", "global.watched": "Gesehen", "global.watchlist": "Beobachtungsliste", + "global.web_access_for_hardware_wallet_disconnected": "Der Webzugriff für die Verbindung von Hardware-Wallets wurde eingestellt. Verbundene Geräte können weiterhin verwendet werden; wenn Probleme auftreten, nutze bitte die App oder die Browser-Erweiterung.", "global.web_feature_not_available_go_to_app": "Diese funktion ist im web nicht verfügbar. Bitte verwenden sie die desktop- oder mobile-app.", "global.website": "Webseite", "global.what_happen": "Was wird passieren:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "Bitte geben Sie das Passwort für dieses Backup ein.", "import_hardware_phrases_warning": "Importiere nicht die Wiederherstellungsphrase deiner Hardware-Wallet. Hardware-Wallet verbinden ↗ stattdessen", "import_phrase_or_private_key": "Phrase oder privaten Schlüssel importieren", + "incorrect_pin": "Falsche PIN. Bitte versuchen Sie es erneut.", "insufficient_fee_append_desc": "basierend auf der maximal geschätzten Gebühr: {amount} {symbol}", "interact_with_contract": "Interagieren mit (An)", "kaspa_official": "Kaspa Offiziell", + "keyless_wallet": "Keyless Wallet", + "keyless_wallet_desc": "Dein schlüsselloses Wallet wird sicher und dezentral über mehrere Faktoren hinweg gespeichert. Stelle den Zugriff auf dein Wallet mit deinem Google- oder Apple-Konto und einer 4-stelligen PIN wieder her.", "learn_how_to_withdraw_crypto_from_exchange": "Erfahre, wie du Krypto-Assets von diesen Börsen zu OneKey abhebst", "learn_more_about_qr_code_wallet": "Erfahren Sie mehr über die QR-based Wallet", "lightning_invoice": "Blitzrechnung", @@ -2044,6 +2060,7 @@ "market.ath_desc": "Der Allzeithoch von {token} war am {time} bei {price}, und der aktuelle Preis liegt um {percent} unter diesem Hoch.", "market.atl_desc": "Der bisherige Tiefststand von {token} wurde am {time} bei {price} erreicht und der aktuelle Preis ist gegenüber diesem Tiefststand um {percent} gestiegen.", "market.cex": "CEX", + "market.change_24h": "Änderung / 24h", "market.chart": "Diagramm", "market.days_since_launch": "Tage seit dem Start", "market.empty_watchlist_desc": "Fügen Sie Ihre Lieblingstoken zur Beobachtungsliste hinzu", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "Nicht verfügbare Netzwerke für das ausgewählte Konto", "network_show_enabled_only": "Nur aktivierte Netzwerke anzeigen", "network_visible_in_all_network_tooltip_title": "Angezeigt in der Ansicht „Alle Netzwerke“", + "new_pin_created": "Neue PIN erstellt", + "new_pin_created_desc": "Sie können Ihre PIN jederzeit in den Einstellungen ändern.", "nft.already_collected": "Dieses NFT wurde bereits gesammelt.", "nft.attributes": "Eigenschaften", "nft.collect_failed": "Das Sammeln von NFT ist fehlgeschlagen, bitte versuchen Sie es erneut.", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "Als Seitenbereich öffnen", "open_in_mobile_app": "In mobiler App öffnen", "open_ordinals_transfer_tutorial_url_message": "Wie überträgt man Ordinals-Vermögenswerte?", + "open_source_secure_sharding": "Open-Source-sichere Abschattung", "p2pkh_desc": "Beginnt mit \"1\". P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "Beginnt mit \"bc1q\". P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "Beginnt mit \"bc1p\". P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "Wir brauchen mehr {token}?", "perps.get_reward": "Prämien sichern", "perps.offline_moblie": "Verbindung verloren. Bitte überprüfe dein Netzwerk und ziehe zum Aktualisieren nach unten.", + "perps.settings_return_to_default_layout": "Zurück zum Standardlayout", "perps.share_position_background": "Hintergrund", "perps.share_position_btn_Share_on_x": "Auf X teilen", "perps.share_position_btn_copy_link": "Link kopieren", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "Aktien", "perps.trade_reward": "Handelsbelohnung", "pick_your_device": "Wähle dein Gerät aus", + "pin_attempts_cooldown": "Versuche es in {seconds} erneut.", + "pin_attempts_remaining": "Falsche PIN eingegeben. Noch ${attemptsRemaining} Versuche übrig.", "preparing_backup_desc": "Einen Moment …", "preparing_backup_title": "Sicherung wird vorbereitet …", "prime.about_cloud_sync": "Über die Cloud-Synchronisation", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "Empfangen mit Ihrer Wallet-Adresse", "receive_from_exchange": "Von Börse empfangen", "receive_token_list_footer_text": "Token nicht gefunden? Suche weiter", + "recovery_phrase_free": "Ohne Wiederherstellungsphrase", "recovery_phrase_screenshot_protected_desc": "Zu Ihrer Vermögenssicherheit wird Ihre Wiederherstellungsphrase niemals in Screenshots angezeigt.", "recovery_phrase_screenshot_protected_title": "Wiederherstellungsphrase geschützt", "referral.accept": "Akzeptieren", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} Adressen", "referral_code_tutorial_label": "Wie erhalte ich einen Empfehlungscode?", "referral_promo_title": "Tritt dem OneKey-Empfehlungsprogramm bei", + "remember_your_pin": "Erinnern Sie sich an Ihre PIN?", + "remember_your_pin_desc": "Nur eine freundliche Erinnerung daran, Ihre PIN im Gedächtnis frisch zu halten. Wir werden uns von Zeit zu Zeit melden.", "remove_account_desc": "Dieses Konto wird entfernt.", "remove_device": "Gerät entfernen", "remove_device_desc": "Dies wird diese Hardware-Wallet von der OneKey App trennen. Sie können sie jederzeit wieder mit der App verbinden.", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "Stellen Sie sicher, dass Sie den Wiederherstellungscode aufgeschrieben haben, bevor Sie die Wallet entfernen. Andernfalls können Sie die Wallet nicht wiederherstellen.", "remove_wallet_double_confirm_message": "Ich habe den Wiederherstellungscode aufgeschrieben", "reset_app_desc": "Dies wird alle Daten löschen, die Sie auf OneKey erstellt haben. Stellen Sie sicher, dass Sie ein ordnungsgemäßes Backup haben, geben Sie \"RESET\" ein, um die App zurückzusetzen", + "reset_pin": "PIN zurücksetzen", "scan.camera_access_denied": "Zugriff auf die Kamera verweigert", "scan.enable_camera_permissions": "OneKey benötigt Kamerazugriff, um QR-Codes zu scannen. Bitte gehen Sie zu den „Einstellungen“ und aktivieren Sie die Kameraberechtigungen, um diese Funktion zu nutzen.", "scan.grant_camera_access_in_expand_view": "Bitte gewähren Sie Kamerazugriff in der erweiterten Ansicht.", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "Scannen, um eine Adresse zu erstellen", "scanning_text": "Scannen", "secure_qr_toast_scan_qr_code_on_device_text": "Kehren Sie zurück, wenn der QR-Code angezeigt wird, klicken Sie auf 'Weiter' und scannen Sie ihn dann.", + "seed_phrase_wallet": "Seed-Phrase-Wallet", "select_connect_app_on_home": "Wähle auf dem Startbildschirm „Connect App Wallet“ aus", "select_onekey_app": "OneKey App auswählen", "select_recovery_phrase_length": "Wählen Sie eine Länge", + "select_your_email": "Wähle deine E-Mail-Adresse aus", + "select_your_email_desc": "Füge eine Wallet mit deinem Google- oder Apple-Konto hinzu", "selected_network": "Ausgewähltes Netzwerk", "selected_network_only_supports_device": "Das ausgewählte Netzwerk unterstützt derzeit nur {deviceType}", "self_troubleshooting": "Selbsthilfe bei Störungen", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "Genehmigung läuft...", "swap.btn_building": "Bestellung wird erstellt...", "swap.ch_status_hold": "Support kontaktieren", + "swap.current_token": "Aktuelle Tokens", "swap.limit_amount": "Verkaufe {num1} {fromToken} für {num2} {toToken}", "swap.loading_content": "Berechnung des besten Preises...", "swap.native_token_max_tip": "Bitte reservieren Sie eine kleine Menge an Token, um die Netzwerkgebühren zu bezahlen, da die Transaktion sonst fehlschlägt.", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "Dienstleistung bereitgestellt von {accelerator}", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "Notiz", "tx_accelerate.speed_up_with_accelerator_dialog_title": "Beschleunigen Sie mit {accelerator}", + "ultra_fast_setup": "Ultraschnelle Einrichtung", "update.all_other_apps_closed": "Alle anderen OneKey Apps und Web-Upgrade-Tools sind geschlossen.", "update.all_other_apps_closed_emoji": "Alle anderen OneKey Apps und Web-Upgrade-Tools sind geschlossen. 🆗", "update.all_updates_complete": "Alle Updates abgeschlossen 👏🏻", diff --git a/packages/shared/src/locale/json/en_US.json b/packages/shared/src/locale/json/en_US.json index 74f692c935ae..314e87f41223 100644 --- a/packages/shared/src/locale/json/en_US.json +++ b/packages/shared/src/locale/json/en_US.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "Rent Energy", "bandwidth_energy.title": "Bandwidth & Energy", "bandwidth_energy.what_is_bandwidth_energy": "What is Bandwidth & Energy on Tron?", + "beginner_friendly": "Beginner-friendly", "bip44__standard": "BIP44 standard", "bluetooth.disable_in_settings": "If you prefer using USB only and don’t want to see this prompt again, go to OneKey app > Settings > Preferences and turn off Bluetooth.", "bluetooth.disabled": "Bluetooth is disabled", @@ -294,6 +295,8 @@ "communication.timeout": "Communication timeout", "confirm_exit_dialog_desc": "Are you sure you want to exit the data migration process?", "confirm_exit_dialog_title": "Confirm exit", + "confirm_your_pin": "Confirm your PIN", + "confirm_your_pin_desc": "If you forget this PIN, you will not be able to recover your wallet on a new device.", "connect_device_to_computer_via_usb": "Connect {deviceLabel} to your computer via USB", "connect_with_qr_code": "Connect with QR Code", "contact_us_instruction": "Need more help?", @@ -306,6 +309,7 @@ "content__normal": "Normal", "content__slow": "Slow", "content__to": "To", + "continue_with_social_platform": "Continue with {platform}", "copy_address_modal_item_create_address_instruction": "Create address", "copy_address_modal_title": "Account address", "copy_anyway": "Copy anyway", @@ -314,6 +318,8 @@ "count_assets": "{count} assets", "count_hidden_assets": "{count} hidden assets", "count_words": "{length} words", + "create_a_pin": "Create a PIN", + "create_a_pin_desc": "This is used to secure your wallet on all your devices. This cannot be recovered.", "create_new_wallet_badge_consists": "Recovery phrase consists of 12 words", "create_new_wallet_badge_handwritten": "Handwritten backup", "create_new_wallet_badge_keep": "Need to keep it safe yourself", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "Most used", "create_new_wallet_badge_supports": "Supports hundreds of networks", "create_new_wallet_learn_more": "The recovery phrase is the core of your wallet’s security. It’s made up of 12 common English words used to create and restore your private key and wallet address. Write it down by hand and store it safely — only you have access to your assets.", + "create_passcode_desc": "You will use this to unlock your wallet.", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "After the standard wallet is created, enter a passphrase to make a hidden wallet.", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "Create hidden wallet", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "No passphrase is needed before displaying the QR code. Tap the ✅ button, show the code, and scan it with the app to create a QR-based standard wallet.", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "Invalid PIN code", "enter_pin.title": "Enter PIN code", "enter_pin_on_app": "Enter PIN on app", + "enter_your_pin": "Enter your PIN", + "enter_your_pin_desc": "This email already has a wallet created. Please enter your PIN to log in.", "explore.add_bookmark": "Add Bookmark", "explore.add_to_whitelist": "add to whitelist", "explore.addresses_count": "{number} addresses", @@ -1072,6 +1081,7 @@ "for_large_assets": "For large assets", "for_reference_only": "Reference only", "forgot_password_no_question_mark": "Forgot password", + "forgot_pin": "Forgot PIN?", "form.address_error_invalid": "Invalid address", "form.address_placeholder": "Address or domain", "form.amount_placeholder": "Enter amount", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "Bridge download", "global.faqs_firmware_detection": "Connection Check", "global.faqs_forgot_pin": "Forgot PIN", - "global.faqs_reset_wallet": "Reset wallet", + "global.faqs_reset_device": "Reset device", "global.favorites": "Favorites", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "Search account name", "global.search_address": "Search address", "global.search_asset": "Search asset", + "global.search_everything": "Search everything", "global.search_no_results_desc": "Try to change the search keyword", "global.search_no_results_title": "No results", + "global.search_placeholder_web": "Search", "global.search_tokens": "Search token", "global.secure_install": "Secure install", "global.security": "Security", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "Watch-only wallet", "global.watched": "Watch-Only", "global.watchlist": "Watchlist", + "global.web_access_for_hardware_wallet_disconnected": "Web access for hardware wallet connection has been discontinued. Connected devices can still be used; if you run into issues, please use the App or the browser extension.", "global.web_feature_not_available_go_to_app": "This feature is not available on web. Please use the desktop or mobile app.", "global.website": "Website", "global.what_happen": "What will happen:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "Please enter the password for this backup.", "import_hardware_phrases_warning": "Don’t import your hardware wallet’s recovery phrase. Connect Hardware Wallet ↗ instead", "import_phrase_or_private_key": "Import phrase or private key", + "incorrect_pin": "Incorrect PIN. Please try again.", "insufficient_fee_append_desc": "based on max est. fee: {amount} {symbol}", "interact_with_contract": "Interact with (To)", "kaspa_official": "Kaspa Official", + "keyless_wallet": "Keyless wallet", + "keyless_wallet_desc": "Your keyless wallet is stored securely and decentralized across multiple factors. Recover access to your wallet with your Google or Apple account and a 4-digit PIN.", "learn_how_to_withdraw_crypto_from_exchange": "Learn how to withdraw crypto assets from these Exchanges to OneKey", "learn_more_about_qr_code_wallet": "Learn more about QR-based wallet", "lightning_invoice": "Lightning Invoice", @@ -2044,6 +2060,7 @@ "market.ath_desc": "{token}’s all-time high was on {time}, at {price}, and the current price is down by {percent} from that high.", "market.atl_desc": "{token}’s all-time low was on {time}, at {price}, and the current price is up by {percent} from that low.", "market.cex": "CEX", + "market.change_24h": "Change / 24h", "market.chart": "Chart", "market.days_since_launch": "Days since launch", "market.empty_watchlist_desc": "Add your favorite tokens to watchlist", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "Unavailable networks for selected account", "network_show_enabled_only": "Only show enabled networks", "network_visible_in_all_network_tooltip_title": "Shown in 'All networks' view", + "new_pin_created": "New PIN Created", + "new_pin_created_desc": "You can change your PIN at anytime through Settings.", "nft.already_collected": "This NFT has already been collected.", "nft.attributes": "Attributes", "nft.collect_failed": "NFT collect failed, please try again.", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "Open as side panel", "open_in_mobile_app": "Open in Mobile App", "open_ordinals_transfer_tutorial_url_message": "How to transfer Ordinals assets?", + "open_source_secure_sharding": "Open source secure shading", "p2pkh_desc": "Starts with \"1\". P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "Starts with \"bc1q\". P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "Starts with \"bc1p\". P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "Need more {token} ?", "perps.get_reward": "Get Rewards", "perps.offline_moblie": "Connection lost. Please try pull to refresh.", + "perps.settings_return_to_default_layout": "Return to default layout", "perps.share_position_background": "Background", "perps.share_position_btn_Share_on_x": "Share on X", "perps.share_position_btn_copy_link": "Copy Link", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "Stocks", "perps.trade_reward": "Trade reward", "pick_your_device": "Pick your device", + "pin_attempts_cooldown": "Try again in {seconds}.", + "pin_attempts_remaining": "Incorrect PIN entered. ${attemptsRemaining} attempts remaining.", "preparing_backup_desc": "Just a moment…", "preparing_backup_title": "Preparing backup…", "prime.about_cloud_sync": "About cloud sync", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "Receive using your wallet address", "receive_from_exchange": "Receive from exchange", "receive_token_list_footer_text": "Can't find token? Try searching", + "recovery_phrase_free": "Recovery phrase free", "recovery_phrase_screenshot_protected_desc": "For your asset security, your recovery phrase will never appear in screenshots.", "recovery_phrase_screenshot_protected_title": "Recovery phrase protected", "referral.accept": "Accept", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} addresses", "referral_code_tutorial_label": "How to get a referral code?", "referral_promo_title": "Join the OneKey Referral Program", + "remember_your_pin": "Remember your PIN?", + "remember_your_pin_desc": "Just a friendly reminder to keep your PIN fresh in memory. We'll check in from time to time.", "remove_account_desc": "This account will be removed.", "remove_device": "Remove device", "remove_device_desc": "This will disconnect this hardware wallet from the OneKey App. You can reconnect it to the App whenever you want.", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "Make sure you've written down the recovery phrase before removing the wallet. Otherwise, you won't be able to recover the wallet.", "remove_wallet_double_confirm_message": "I've written down the recovery phrase", "reset_app_desc": "This will delete all the data you have created on OneKey. After making sure that you have a proper backup, enter \"RESET\" to reset the App", + "reset_pin": "Reset PIN", "scan.camera_access_denied": "Camera access denied", "scan.enable_camera_permissions": "OneKey requires camera access to scan QR codes. Please go to “Settings” and enable camera permissions to use this feature.", "scan.grant_camera_access_in_expand_view": "Please grant camera access in the expand view.", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "Scan to create an address", "scanning_text": "Scanning", "secure_qr_toast_scan_qr_code_on_device_text": "Return when the QR code shows, click 'Next', then scan it.", + "seed_phrase_wallet": "Seed phrase wallet", "select_connect_app_on_home": "Select Connect App Wallet on home screen", "select_onekey_app": "Select OneKey App", "select_recovery_phrase_length": "Select a length", + "select_your_email": "Select your email", + "select_your_email_desc": "Add a wallet with your Google or Apple account", "selected_network": "Selected network", "selected_network_only_supports_device": "The selected network currently only supports {deviceType}", "self_troubleshooting": "Self-troubleshooting", @@ -3283,7 +3313,7 @@ "settings.upload_state_logs": "Upload state logs", "settings.uploading_logs_progress": "Uploading logs… {progress}%", "settings.user_agreement": "User agreement", - "settings.version_versionnum": "version {versionNum}", + "settings.version_versionnum": "Version {versionNum}", "settings.view_address_in_explorer": "View address in explorer", "settings.view_transaction_in_explorer": "View transaction in explorer", "settings.whats_new": "What’s new", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "Approving...", "swap.btn_building": "Building order...", "swap.ch_status_hold": "Contact support", + "swap.current_token": "Current tokens", "swap.limit_amount": "Sell {num1} {fromToken} for {num2} {toToken}", "swap.loading_content": "Calculating the best price...", "swap.native_token_max_tip": "Please reserve a small amount of tokens to pay for network fees, otherwise the transaction will fail.", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "Service provided by {accelerator}", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "Note", "tx_accelerate.speed_up_with_accelerator_dialog_title": "Speed up with {accelerator}", + "ultra_fast_setup": "Ultra-fast setup", "update.all_other_apps_closed": "All other OneKey Apps and web upgrade tools are closed.", "update.all_other_apps_closed_emoji": "All other OneKey Apps and web upgrade tools are closed. 🆗", "update.all_updates_complete": "All updates complete 👏🏻", diff --git a/packages/shared/src/locale/json/es.json b/packages/shared/src/locale/json/es.json index e4ede405a588..d735b43487dc 100644 --- a/packages/shared/src/locale/json/es.json +++ b/packages/shared/src/locale/json/es.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "Alquiler de Energía", "bandwidth_energy.title": "Ancho de banda y energía", "bandwidth_energy.what_is_bandwidth_energy": "¿Qué es Ancho de Banda y Energía en Tron?", + "beginner_friendly": "Apto para principiantes", "bip44__standard": "Estándar BIP44", "bluetooth.disable_in_settings": "Si prefieres usar solo USB y no quieres volver a ver este mensaje, ve a la app de OneKey > Configuración > Preferencias y desactiva Bluetooth.", "bluetooth.disabled": "Bluetooth está desactivado", @@ -294,6 +295,8 @@ "communication.timeout": "Tiempo de espera de comunicación agotado", "confirm_exit_dialog_desc": "¿Estás seguro de que quieres salir del proceso de migración de datos?", "confirm_exit_dialog_title": "Confirmar salida", + "confirm_your_pin": "Confirma tu PIN", + "confirm_your_pin_desc": "Si olvidas este PIN, no podrás recuperar tu billetera en un dispositivo nuevo.", "connect_device_to_computer_via_usb": "Conecta {deviceLabel} a tu computadora por USB", "connect_with_qr_code": "Conectar con código QR", "contact_us_instruction": "¿Necesitas más ayuda?", @@ -306,6 +309,7 @@ "content__normal": "Normal", "content__slow": "Lento", "content__to": "Para", + "continue_with_social_platform": "Continuar con {platform}", "copy_address_modal_item_create_address_instruction": "Crear dirección", "copy_address_modal_title": "Dirección de cuenta", "copy_anyway": "Copiar de todos modos", @@ -314,6 +318,8 @@ "count_assets": "{count} activos", "count_hidden_assets": "{count} activos ocultos", "count_words": "{length} palabras", + "create_a_pin": "Crear un PIN", + "create_a_pin_desc": "Esto se usa para proteger tu billetera en todos tus dispositivos. Esto no se puede recuperar.", "create_new_wallet_badge_consists": "La frase de recuperación consta de 12 palabras", "create_new_wallet_badge_handwritten": "Copia de seguridad manuscrita", "create_new_wallet_badge_keep": "Necesitas mantenerlo seguro tú mismo", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "Más usado", "create_new_wallet_badge_supports": "Compatible con cientos de redes", "create_new_wallet_learn_more": "La frase de recuperación es el núcleo de la seguridad de tu billetera. Está compuesta por 12 palabras comunes en inglés que se usan para crear y restaurar tu clave privada y la dirección de tu billetera. Escríbela a mano y guárdala en un lugar seguro; solo tú tienes acceso a tus activos.", + "create_passcode_desc": "Usarás esto para desbloquear tu billetera.", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "Después de crear la cartera estándar, ingresa una passphrase para hacer una cartera oculta.", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "Crea una cartera oculta", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "No se necesita una passphrase antes de mostrar el código QR. Toca el botón ✅, muestra el código y escanéalo con la aplicación para crear una cartera estándar basada en QR.", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "Código PIN inválido", "enter_pin.title": "Ingrese el código PIN", "enter_pin_on_app": "Ingresa el Pin en la aplicación", + "enter_your_pin": "Ingrese su PIN", + "enter_your_pin_desc": "Este correo electrónico ya tiene una billetera creada. Ingresa tu PIN para iniciar sesión.", "explore.add_bookmark": "Añadir marcador", "explore.add_to_whitelist": "agregar a la lista blanca", "explore.addresses_count": "{number} direcciones", @@ -1072,6 +1081,7 @@ "for_large_assets": "Para activos grandes", "for_reference_only": "Solo para referencia", "forgot_password_no_question_mark": "¿Olvidaste tu contraseña?", + "forgot_pin": "¿Olvidaste tu PIN?", "form.address_error_invalid": "Dirección inválida", "form.address_placeholder": "Dirección o dominio", "form.amount_placeholder": "Ingrese la cantidad", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "Descargar puente de comunicación", "global.faqs_firmware_detection": "Prueba de conexión", "global.faqs_forgot_pin": "¿Olvidaste tu PIN?", - "global.faqs_reset_wallet": "Restablecer wallet", + "global.faqs_reset_device": "Restablecer dispositivo", "global.favorites": "Favoritos", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "Buscar nombre de cuenta", "global.search_address": "Buscar dirección", "global.search_asset": "Buscar activo", + "global.search_everything": "Buscar todo", "global.search_no_results_desc": "Intenta cambiar la palabra clave de búsqueda", "global.search_no_results_title": "No hay resultados", + "global.search_placeholder_web": "Buscar", "global.search_tokens": "Buscar token", "global.secure_install": "Instalación segura", "global.security": "Seguridad", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "Cartera de solo visualización", "global.watched": "Visto", "global.watchlist": "Lista de seguimiento", + "global.web_access_for_hardware_wallet_disconnected": "Se ha descontinuado el acceso web para la conexión de monederos de hardware. Los dispositivos conectados se pueden seguir usando; si tienes problemas, utiliza la App o la extensión del navegador.", "global.web_feature_not_available_go_to_app": "Esta función no está disponible en la web. Utilice la aplicación de escritorio o móvil.", "global.website": "Sitio web", "global.what_happen": "Qué sucederá:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "Introduce la contraseña de esta copia de seguridad.", "import_hardware_phrases_warning": "No importes la frase de recuperación de tu hardware wallet. Conectar hardware wallet ↗ en su lugar", "import_phrase_or_private_key": "Importar frase o clave privada", + "incorrect_pin": "PIN incorrecto. Inténtalo de nuevo.", "insufficient_fee_append_desc": "basado en la tarifa máxima estimada: {amount} {symbol}", "interact_with_contract": "Interactuar con (Para)", "kaspa_official": "Kaspa Oficial", + "keyless_wallet": "Billetera sin clave privada", + "keyless_wallet_desc": "Tu monedero sin llaves se almacena de forma segura y descentralizada a través de múltiples factores. Recupera el acceso a tu monedero con tu cuenta de Google o Apple y un PIN de 4 dígitos.", "learn_how_to_withdraw_crypto_from_exchange": "Aprende a retirar criptoactivos de estos Exchanges a OneKey", "learn_more_about_qr_code_wallet": "Más información sobre la billetera QR-based", "lightning_invoice": "Factura de Rayo", @@ -2044,6 +2060,7 @@ "market.ath_desc": "El máximo histórico de {token} fue el {time}, a {price}, y el precio actual ha bajado un {percent} desde ese máximo.", "market.atl_desc": "El mínimo histórico de {token} fue el {time} , a {price} , y el precio actual ha subido un {percent} desde ese mínimo.", "market.cex": "CEX", + "market.change_24h": "Cambio / 24h", "market.chart": "Cuadro", "market.days_since_launch": "Días desde el lanzamiento", "market.empty_watchlist_desc": "Agrega tus tokens favoritos a la lista de seguimiento", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "Redes no disponibles para la cuenta seleccionada", "network_show_enabled_only": "Mostrar solo las redes habilitadas", "network_visible_in_all_network_tooltip_title": "Mostrado en vista de 'Todas las redes'", + "new_pin_created": "Nuevo PIN creado", + "new_pin_created_desc": "Puedes cambiar tu PIN en cualquier momento desde Configuración.", "nft.already_collected": "Este NFT ya ha sido recolectado.", "nft.attributes": "Atributos", "nft.collect_failed": "La recolección de NFT falló, por favor intenta de nuevo.", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "Abrir como panel lateral", "open_in_mobile_app": "Abrir en la aplicación móvil", "open_ordinals_transfer_tutorial_url_message": "¿Cómo transferir activos Ordinals?", + "open_source_secure_sharding": "Sombreado seguro de código abierto", "p2pkh_desc": "Comienza con \"1\". P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "Comienza con \"bc1q\". P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "Comienza con \"bc1p\". P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "Necesita más {token}?", "perps.get_reward": "Obtenga recompensas", "perps.offline_moblie": "Conexión perdida. Verifica tu red y desliza hacia abajo para actualizar.", + "perps.settings_return_to_default_layout": "Volver al diseño predeterminado", "perps.share_position_background": "Fondo", "perps.share_position_btn_Share_on_x": "Compartir en X", "perps.share_position_btn_copy_link": "Copiar enlace", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "Cepo", "perps.trade_reward": "Recompensa comercial", "pick_your_device": "Elige tu dispositivo", + "pin_attempts_cooldown": "Inténtalo de nuevo en {seconds}.", + "pin_attempts_remaining": "PIN incorrecto. Quedan ${attemptsRemaining} intentos.", "preparing_backup_desc": "Un momento…", "preparing_backup_title": "Preparando la copia de seguridad…", "prime.about_cloud_sync": "Acerca de la sincronización con la nube", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "Recibir usando tu dirección de billetera", "receive_from_exchange": "Recibir desde el exchange", "receive_token_list_footer_text": "¿No encuentras el token? Intenta buscarlo", + "recovery_phrase_free": "Sin frase de recuperación", "recovery_phrase_screenshot_protected_desc": "Para la seguridad de tus activos, tu frase de recuperación nunca aparecerá en capturas de pantalla.", "recovery_phrase_screenshot_protected_title": "Frase de recuperación protegida", "referral.accept": "Aceptar", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} direcciones", "referral_code_tutorial_label": "¿Cómo obtener un código de recomendación?", "referral_promo_title": "Únete al Programa de Referidos de OneKey", + "remember_your_pin": "¿Recuerdas tu PIN?", + "remember_your_pin_desc": "Solo un recordatorio amistoso para que mantengas tu PIN fresco en la memoria. Nos pondremos en contacto de vez en cuando.", "remove_account_desc": "Esta cuenta será eliminada.", "remove_device": "Eliminar dispositivo", "remove_device_desc": "Esto desconectará este monedero de hardware de la OneKey App. Puedes volver a conectarlo a la App cuando quieras.", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "Asegúrate de haber anotado la frase de recuperación antes de eliminar la billetera. De lo contrario, no podrás recuperar la billetera.", "remove_wallet_double_confirm_message": "He escrito la frase de recuperación", "reset_app_desc": "Esto eliminará todos los datos que hayas creado en OneKey. Después de asegurarte de que tienes una copia de seguridad adecuada, ingresa \"RESET\" para reiniciar la App", + "reset_pin": "Restablecer PIN", "scan.camera_access_denied": "Acceso a la cámara denegado", "scan.enable_camera_permissions": "OneKey requiere acceso a la cámara para escanear códigos QR. Por favor, vaya a \"Configuración\" y habilite los permisos de la cámara para usar esta función.", "scan.grant_camera_access_in_expand_view": "Por favor, concede acceso a la cámara en la vista expandida.", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "Escanea para crear una dirección", "scanning_text": "Exploración", "secure_qr_toast_scan_qr_code_on_device_text": "Regresa cuando aparezca el código QR, haz clic en 'Siguiente', luego escanéalo.", + "seed_phrase_wallet": "Billetera de frase semilla", "select_connect_app_on_home": "Selecciona Connect App Wallet en la pantalla de inicio", "select_onekey_app": "Selecciona la App OneKey", "select_recovery_phrase_length": "Seleccione una longitud", + "select_your_email": "Selecciona tu correo electrónico", + "select_your_email_desc": "Agrega una cartera con tu cuenta de Google o Apple", "selected_network": "Red seleccionada", "selected_network_only_supports_device": "La red seleccionada actualmente solo admite {deviceType}", "self_troubleshooting": "Solución de problemas por cuenta propia", @@ -3283,7 +3313,7 @@ "settings.upload_state_logs": "Subir registros de estado", "settings.uploading_logs_progress": "Subiendo registros… {progress}%", "settings.user_agreement": "Acuerdo de usuario", - "settings.version_versionnum": "versión {versionNum}", + "settings.version_versionnum": "Versión {versionNum}", "settings.view_address_in_explorer": "Ver dirección en el explorador", "settings.view_transaction_in_explorer": "Ver transacción en el explorador", "settings.whats_new": "¿Qué hay de nuevo?", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "Aprobando...", "swap.btn_building": "Construyendo orden...", "swap.ch_status_hold": "Contactar con soporte técnico", + "swap.current_token": "Tokens actuales", "swap.limit_amount": "Vender {num1} {fromToken} por {num2} {toToken}", "swap.loading_content": "Calculando el mejor precio...", "swap.native_token_max_tip": "Por favor, reserve una pequeña cantidad de tokens para pagar las tarifas de la red; de lo contrario, la transacción fallará.", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "Servicio proporcionado por {accelerator}", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "Nota", "tx_accelerate.speed_up_with_accelerator_dialog_title": "Acelera con {accelerator}", + "ultra_fast_setup": "Configuración ultrarrápida", "update.all_other_apps_closed": "Todas las demás aplicaciones de OneKey y herramientas de actualización web están cerradas.", "update.all_other_apps_closed_emoji": "Todas las demás aplicaciones de OneKey y herramientas de actualización web están cerradas. 🆗", "update.all_updates_complete": "Todas las actualizaciones completas 👏🏻", diff --git a/packages/shared/src/locale/json/fr_FR.json b/packages/shared/src/locale/json/fr_FR.json index 4103fbe22d3c..267f8b0cf626 100644 --- a/packages/shared/src/locale/json/fr_FR.json +++ b/packages/shared/src/locale/json/fr_FR.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "Louer Énergie", "bandwidth_energy.title": "Bande passante & Énergie", "bandwidth_energy.what_is_bandwidth_energy": "Qu'est-ce que la bande passante et l'énergie sur Tron ?", + "beginner_friendly": "Convivial pour les débutants", "bip44__standard": "Standard BIP44", "bluetooth.disable_in_settings": "Si vous préférez utiliser uniquement USB et ne souhaitez plus voir cette invite, allez dans l'application OneKey > Paramètres > Préférences et désactivez le Bluetooth.", "bluetooth.disabled": "Le Bluetooth est désactivé", @@ -294,6 +295,8 @@ "communication.timeout": "Délai d'attente de communication", "confirm_exit_dialog_desc": "Êtes-vous sûr de vouloir quitter le processus de migration des données ?", "confirm_exit_dialog_title": "Confirmer la sortie", + "confirm_your_pin": "Confirmez votre code PIN", + "confirm_your_pin_desc": "Si vous oubliez ce code PIN, vous ne pourrez pas récupérer votre portefeuille sur un nouvel appareil.", "connect_device_to_computer_via_usb": "Connectez {deviceLabel} à votre ordinateur via USB", "connect_with_qr_code": "Se connecter avec un code QR", "contact_us_instruction": "Besoin d'aide supplémentaire ?", @@ -306,6 +309,7 @@ "content__normal": "Normal", "content__slow": "Lent", "content__to": "À", + "continue_with_social_platform": "Continuer avec {platform}", "copy_address_modal_item_create_address_instruction": "Créer une adresse", "copy_address_modal_title": "Adresse du compte", "copy_anyway": "Copier quand même", @@ -314,6 +318,8 @@ "count_assets": "{count} actifs", "count_hidden_assets": "{count} actifs cachés", "count_words": "{length} mots", + "create_a_pin": "Créer un code PIN", + "create_a_pin_desc": "Ceci est utilisé pour sécuriser votre portefeuille sur tous vos appareils. Cela ne peut pas être récupéré.", "create_new_wallet_badge_consists": "La phrase de récupération se compose de 12 mots", "create_new_wallet_badge_handwritten": "Sauvegarde manuscrite", "create_new_wallet_badge_keep": "Vous devez le garder en sécurité vous-même", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "Les plus utilisés", "create_new_wallet_badge_supports": "Prend en charge des centaines de réseaux", "create_new_wallet_learn_more": "La phrase de récupération est au cœur de la sécurité de votre portefeuille. Elle est composée de 12 mots anglais courants utilisés pour créer et restaurer votre clé privée et l'adresse de votre portefeuille. Notez-la à la main et conservez-la en lieu sûr — vous seul avez accès à vos actifs.", + "create_passcode_desc": "Vous utiliserez ceci pour déverrouiller votre portefeuille.", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "Après la création du portefeuille standard, entrez une passphrase pour créer un portefeuille caché.", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "Créer un portefeuille caché", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "Aucune passphrase n'est nécessaire avant d'afficher le code QR. Appuyez sur le bouton ✅, montrez le code et scannez-le avec l'application pour créer un portefeuille standard basé sur QR.", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "Code PIN invalide", "enter_pin.title": "Entrez le code PIN", "enter_pin_on_app": "Entrez le code PIN sur l'application", + "enter_your_pin": "Saisissez votre code PIN", + "enter_your_pin_desc": "Cet e-mail possède déjà un portefeuille. Veuillez saisir votre code PIN pour vous connecter.", "explore.add_bookmark": "Ajouter un marque-page", "explore.add_to_whitelist": "ajouter à la liste blanche", "explore.addresses_count": "{number} adresses", @@ -1072,6 +1081,7 @@ "for_large_assets": "Pour les actifs importants", "for_reference_only": "Pour référence uniquement", "forgot_password_no_question_mark": "Mot de passe oublié", + "forgot_pin": "Code PIN oublié ?", "form.address_error_invalid": "Adresse invalide", "form.address_placeholder": "Adresse ou domaine", "form.amount_placeholder": "Entrez le montant", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "Télécharger le pont de communication", "global.faqs_firmware_detection": "Test de connexion", "global.faqs_forgot_pin": "PIN oublié", - "global.faqs_reset_wallet": "Réinitialiser le wallet", + "global.faqs_reset_device": "Réinitialiser l'appareil", "global.favorites": "Favoris", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "Rechercher le nom du compte", "global.search_address": "Rechercher une adresse", "global.search_asset": "Rechercher un actif", + "global.search_everything": "Tout rechercher", "global.search_no_results_desc": "Essayez de changer le mot-clé de recherche", "global.search_no_results_title": "Aucun résultat", + "global.search_placeholder_web": "Recherche", "global.search_tokens": "Rechercher un jeton", "global.secure_install": "Installation sécurisée", "global.security": "Sécurité", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "Portefeuille en lecture seule", "global.watched": "Regardé", "global.watchlist": "Liste de surveillance", + "global.web_access_for_hardware_wallet_disconnected": "L'accès Web pour la connexion du portefeuille matériel a été interrompu. Les appareils connectés peuvent toujours être utilisés ; si vous rencontrez des problèmes, veuillez utiliser l'App ou l'extension de navigateur.", "global.web_feature_not_available_go_to_app": "Cette fonctionnalité n'est pas disponible sur le web. Veuillez utiliser l'application de bureau ou mobile.", "global.website": "Site web", "global.what_happen": "Ce qui va se passer :", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "Veuillez saisir le mot de passe pour cette sauvegarde.", "import_hardware_phrases_warning": "N'importez pas la phrase de récupération de votre portefeuille matériel. Connecter un portefeuille matériel ↗ à la place", "import_phrase_or_private_key": "Importer une phrase ou une clé privée", + "incorrect_pin": "Code PIN incorrect. Veuillez réessayer.", "insufficient_fee_append_desc": "basé sur les frais max estimés : {amount} {symbol}", "interact_with_contract": "Interagir avec (À)", "kaspa_official": "Kaspa Officiel", + "keyless_wallet": "Portefeuille sans clé", + "keyless_wallet_desc": "Votre portefeuille sans clé est stocké de manière sécurisée et décentralisée sur plusieurs facteurs. Récupérez l'accès à votre portefeuille avec votre compte Google ou Apple et un code PIN à 4 chiffres.", "learn_how_to_withdraw_crypto_from_exchange": "Découvrez comment retirer des actifs crypto de ces plateformes d'échange vers OneKey", "learn_more_about_qr_code_wallet": "En savoir plus sur le portefeuille QR-based", "lightning_invoice": "Facture Éclair", @@ -2044,6 +2060,7 @@ "market.ath_desc": "Le record historique de {token} a été atteint le {time}, à {price}, et le prix actuel est en baisse de {percent} par rapport à ce sommet.", "market.atl_desc": "Le plus bas historique de {token} a eu lieu le {time} , à {price} , et le prix actuel est en hausse de {percent} par rapport à ce plus bas.", "market.cex": "CEX", + "market.change_24h": "Variation / 24h", "market.chart": "Graphique", "market.days_since_launch": "Jours depuis le lancement", "market.empty_watchlist_desc": "Ajoutez vos jetons préférés à la liste de surveillance", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "Réseaux indisponibles pour le compte sélectionné", "network_show_enabled_only": "Afficher uniquement les réseaux activés", "network_visible_in_all_network_tooltip_title": "Affiché dans la vue 'Tous les réseaux'", + "new_pin_created": "Nouveau code PIN créé", + "new_pin_created_desc": "Vous pouvez modifier votre code PIN à tout moment dans les Paramètres.", "nft.already_collected": "Ce NFT a déjà été collecté.", "nft.attributes": "Attributs", "nft.collect_failed": "La collecte de NFT a échoué, veuillez réessayer.", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "Ouvrir en panneau", "open_in_mobile_app": "Ouvrir dans l'application mobile", "open_ordinals_transfer_tutorial_url_message": "Comment transférer les actifs Ordinals ?", + "open_source_secure_sharding": "Ombrage sécurisé open source", "p2pkh_desc": "Commence par \"1\". P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "Commence par \"bc1q\". P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "Commence par \"bc1p\". P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "J'en ai besoin de plus {token}?", "perps.get_reward": "Obtenez des récompenses", "perps.offline_moblie": "Connexion perdue. Veuillez vérifier votre réseau et essayer de tirer pour actualiser.", + "perps.settings_return_to_default_layout": "Revenir à la disposition par défaut", "perps.share_position_background": "Contexte", "perps.share_position_btn_Share_on_x": "Partager sur X", "perps.share_position_btn_copy_link": "Copier le lien", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "Actions", "perps.trade_reward": "Récompense commerciale", "pick_your_device": "Choisissez votre appareil", + "pin_attempts_cooldown": "Réessayez dans {seconds}.", + "pin_attempts_remaining": "Code PIN incorrect. Il reste ${attemptsRemaining} tentatives.", "preparing_backup_desc": "Un instant…", "preparing_backup_title": "Préparation de la sauvegarde…", "prime.about_cloud_sync": "À propos de la synchronisation cloud", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "Recevoir en utilisant votre adresse de portefeuille", "receive_from_exchange": "Recevoir depuis l'échange", "receive_token_list_footer_text": "Vous ne trouvez pas de jeton ? Essayez de rechercher", + "recovery_phrase_free": "Sans phrase de récupération", "recovery_phrase_screenshot_protected_desc": "Pour la sécurité de vos actifs, votre phrase de récupération n'apparaîtra jamais dans les captures d'écran.", "recovery_phrase_screenshot_protected_title": "Phrase de récupération protégée", "referral.accept": "Accepter", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} adresses", "referral_code_tutorial_label": "Comment obtenir un code de parrainage ?", "referral_promo_title": "Rejoignez le programme de parrainage OneKey", + "remember_your_pin": "Vous souvenez-vous de votre code PIN ?", + "remember_your_pin_desc": "Juste un rappel amical pour garder votre code PIN en mémoire. Nous vérifierons de temps en temps.", "remove_account_desc": "Ce compte sera supprimé.", "remove_device": "Supprimer l'appareil", "remove_device_desc": "Cela va déconnecter ce portefeuille matériel de l'application OneKey. Vous pouvez le reconnecter à l'application à tout moment.", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "Assurez-vous d'avoir noté la phrase de récupération avant de supprimer le portefeuille. Sinon, vous ne pourrez pas récupérer le portefeuille.", "remove_wallet_double_confirm_message": "J'ai noté la phrase de récupération", "reset_app_desc": "Cela supprimera toutes les données que vous avez créées sur OneKey. Après vous être assuré d'avoir une sauvegarde appropriée, entrez \"RESET\" pour réinitialiser l'application", + "reset_pin": "Réinitialiser le code PIN", "scan.camera_access_denied": "Accès à la caméra refusé", "scan.enable_camera_permissions": "OneKey nécessite l'accès à la caméra pour scanner les codes QR. Veuillez aller dans \"Paramètres\" et activer les autorisations de la caméra pour utiliser cette fonctionnalité.", "scan.grant_camera_access_in_expand_view": "Veuillez autoriser l'accès à la caméra dans la vue étendue.", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "Scannez pour créer une adresse", "scanning_text": "Balayage", "secure_qr_toast_scan_qr_code_on_device_text": "Revenez lorsque le code QR apparaît, cliquez sur 'Suivant', puis scannez-le.", + "seed_phrase_wallet": "Portefeuille à phrase de récupération", "select_connect_app_on_home": "Sélectionnez Connecter le portefeuille App sur l'écran d'accueil", "select_onekey_app": "Sélectionner OneKey App", "select_recovery_phrase_length": "Sélectionnez une longueur", + "select_your_email": "Sélectionnez votre e-mail", + "select_your_email_desc": "Ajouter un portefeuille avec votre compte Google ou Apple", "selected_network": "Réseau sélectionné", "selected_network_only_supports_device": "Le réseau sélectionné ne prend actuellement en charge que {deviceType}", "self_troubleshooting": "Auto-dépannage", @@ -3283,7 +3313,7 @@ "settings.upload_state_logs": "Téléverser les journaux d'état", "settings.uploading_logs_progress": "Téléchargement des journaux… {progress} %", "settings.user_agreement": "Accord d'utilisateur", - "settings.version_versionnum": "version {versionNum}", + "settings.version_versionnum": "Version {versionNum}", "settings.view_address_in_explorer": "Voir l'adresse dans l'explorateur", "settings.view_transaction_in_explorer": "Voir la transaction dans l'explorateur", "settings.whats_new": "Quoi de neuf", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "Approbation...", "swap.btn_building": "Construction de la commande...", "swap.ch_status_hold": "Contacter le support", + "swap.current_token": "Jetons actuels", "swap.limit_amount": "Vendre {num1} {fromToken} contre {num2} {toToken}", "swap.loading_content": "Calcul en cours du meilleur prix...", "swap.native_token_max_tip": "Veuillez réserver une petite quantité de jetons pour payer les frais de réseau, sinon la transaction échouera.", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "Service fourni par {accelerator}", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "Note", "tx_accelerate.speed_up_with_accelerator_dialog_title": "Accélérez avec {accelerator}", + "ultra_fast_setup": "Configuration ultra-rapide", "update.all_other_apps_closed": "Toutes les autres applications OneKey et les outils de mise à niveau web sont fermés.", "update.all_other_apps_closed_emoji": "Toutes les autres applications OneKey et outils de mise à niveau web sont fermés. 🆗", "update.all_updates_complete": "Toutes les mises à jour sont terminées 👏🏻", diff --git a/packages/shared/src/locale/json/hi_IN.json b/packages/shared/src/locale/json/hi_IN.json index ff6d010fc0d8..988efb762afa 100644 --- a/packages/shared/src/locale/json/hi_IN.json +++ b/packages/shared/src/locale/json/hi_IN.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "रेंट एनर्जी", "bandwidth_energy.title": "बैंडविड्थ और ऊर्जा", "bandwidth_energy.what_is_bandwidth_energy": "ट्रॉन पर बैंडविड्थ और ऊर्जा क्या है?", + "beginner_friendly": "शुरुआती-अनुकूल", "bip44__standard": "BIP44 मानक", "bluetooth.disable_in_settings": "यदि आप केवल USB का उपयोग करना पसंद करते हैं और इस प्रॉम्प्ट को फिर से नहीं देखना चाहते हैं, तो OneKey ऐप > सेटिंग्स > प्रेफरेंसेस में जाएँ और ब्लूटूथ को बंद कर दें।", "bluetooth.disabled": "ब्लूटूथ अक्षम है", @@ -294,6 +295,8 @@ "communication.timeout": "संचार टाइमआउट", "confirm_exit_dialog_desc": "क्या आप निश्चित हैं कि आप डाटा माइग्रेशन प्रक्रिया से बाहर निकलना चाहते हैं?", "confirm_exit_dialog_title": "निकास की पुष्टि करें", + "confirm_your_pin": "अपने पिन की पुष्टि करें", + "confirm_your_pin_desc": "यदि आप यह PIN भूल जाते हैं, तो आप नए डिवाइस पर अपने वॉलेट को पुनर्प्राप्त नहीं कर पाएंगे।", "connect_device_to_computer_via_usb": "USB के माध्यम से {deviceLabel} को अपने कंप्यूटर से कनेक्ट करें", "connect_with_qr_code": "QR कोड से कनेक्ट करें", "contact_us_instruction": "और अधिक मदद की आवश्यकता है?", @@ -306,6 +309,7 @@ "content__normal": "सामान्य", "content__slow": "धीमा", "content__to": "करने के लिए", + "continue_with_social_platform": "{platform} के साथ जारी रखें", "copy_address_modal_item_create_address_instruction": "पता बनाएं", "copy_address_modal_title": "खाता पता", "copy_anyway": "फिर भी कॉपी करें", @@ -314,6 +318,8 @@ "count_assets": "{count} संपत्तियां", "count_hidden_assets": "{count} छिपे संपत्ति", "count_words": "{length} शब्द", + "create_a_pin": "पिन बनाएं", + "create_a_pin_desc": "यह आपके सभी डिवाइस पर आपके वॉलेट को सुरक्षित करने के लिए उपयोग किया जाता है। इसे पुनर्प्राप्त नहीं किया जा सकता।", "create_new_wallet_badge_consists": "रिकवरी वाक्यांश 12 शब्दों से बना होता है", "create_new_wallet_badge_handwritten": "हस्तलिखित बैकअप", "create_new_wallet_badge_keep": "आपको इसे खुद ही सुरक्षित रखना होगा", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "सबसे अधिक उपयोग किया गया", "create_new_wallet_badge_supports": "सैकड़ों नेटवर्क का समर्थन करता है", "create_new_wallet_learn_more": "रिकवरी फ़्रेज़ आपके वॉलेट की सुरक्षा का मूल है। यह 12 सामान्य अंग्रेज़ी शब्दों से बना होता है, जिनका उपयोग आपकी प्राइवेट की और वॉलेट एड्रेस बनाने और पुनर्स्थापित करने के लिए किया जाता है। इसे हाथ से लिखें और सुरक्षित जगह पर रखें — आपके असेट्स तक पहुँच सिर्फ़ आपके पास है।", + "create_passcode_desc": "आप इसका उपयोग अपने वॉलेट को अनलॉक करने के लिए करेंगे।", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "मानक वॉलेट बनाने के बाद, एक hidden वॉलेट बनाने के लिए passphrase दर्ज करें।", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "छिपा हुआ वॉलेट बनाएं", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "QR कोड प्रदर्शित करने से पहले कोई passphrase की आवश्यकता नहीं है। ✅ बटन को छूने के बाद, कोड दिखाएं, और इसे ऐप के साथ स्कैन करें ताकि QR-based स्टैंडर्ड वॉलेट बनाया जा सके।", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "अमान्य पिन कोड", "enter_pin.title": "पिन कोड दर्ज करें", "enter_pin_on_app": "ऐप पर पिन दर्ज करें", + "enter_your_pin": "अपना पिन दर्ज करें", + "enter_your_pin_desc": "इस ईमेल के लिए पहले से एक वॉलेट बनाया गया है। कृपया लॉग इन करने के लिए अपना पिन दर्ज करें।", "explore.add_bookmark": "बुकमार्क जोड़ें", "explore.add_to_whitelist": "व्हाइटलिस्ट में जोड़ें", "explore.addresses_count": "{number} पते", @@ -1072,6 +1081,7 @@ "for_large_assets": "बड़ी संपत्तियों के लिए", "for_reference_only": "केवल संदर्भ हेतु", "forgot_password_no_question_mark": "पासवर्ड भूल गए", + "forgot_pin": "पिन भूल गए?", "form.address_error_invalid": "अमान्य पता", "form.address_placeholder": "पता या डोमेन", "form.amount_placeholder": "राशि दर्ज करें", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "हार्डवेयर ब्रिज डाउनलोड", "global.faqs_firmware_detection": "कनेक्शन जांच", "global.faqs_forgot_pin": "PIN भूल गए", - "global.faqs_reset_wallet": "वॉलेट रीसेट", + "global.faqs_reset_device": "डिवाइस रीसेट करें", "global.favorites": "पसंदीदा", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "खाता नाम खोजें", "global.search_address": "पता खोजें", "global.search_asset": "संपत्ति खोजें", + "global.search_everything": "सब कुछ खोजें", "global.search_no_results_desc": "खोज कीवर्ड को बदलने की कोशिश करें", "global.search_no_results_title": "कोई परिणाम नहीं", + "global.search_placeholder_web": "खोजें", "global.search_tokens": "टोकन खोजें", "global.secure_install": "सुरक्षित स्थापना", "global.security": "सुरक्षा", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "केवल देखने वाला वॉलेट", "global.watched": "देखा", "global.watchlist": "वॉचलिस्ट", + "global.web_access_for_hardware_wallet_disconnected": "हार्डवेयर वॉलेट कनेक्शन के लिए वेब एक्सेस बंद कर दिया गया है। कनेक्टेड डिवाइस अभी भी उपयोग किए जा सकते हैं; यदि आपको कोई समस्या आती है, तो कृपया App या ब्राउज़र एक्सटेंशन का उपयोग करें।", "global.web_feature_not_available_go_to_app": "यह सुविधा वेब पर उपलब्ध नहीं है। कृपया डेस्कटॉप या मोबाइल ऐप का उपयोग करें।", "global.website": "वेबसाइट", "global.what_happen": "क्या होगा:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "कृपया इस बैकअप के लिए पासवर्ड दर्ज करें।", "import_hardware_phrases_warning": "अपने हार्डवेयर वॉलेट का रिकवरी फ़्रेज आयात न करें। हार्डवेयर वॉलेट कनेक्ट करें ↗", "import_phrase_or_private_key": "वाक्यांश या निजी कुंजी आयात करें", + "incorrect_pin": "गलत पिन। कृपया पुनः प्रयास करें।", "insufficient_fee_append_desc": "अधिकतम अनुमानित शुल्क के आधार पर: {amount} {symbol}", "interact_with_contract": "(से) संवाद करें", "kaspa_official": "Kaspa आधिकारिक", + "keyless_wallet": "कीलेस वॉलेट", + "keyless_wallet_desc": "आपका कीलेस वॉलेट कई कारकों में सुरक्षित और विकेंद्रीकृत तरीके से संग्रहीत है। अपने Google या Apple खाते और 4-अंकीय PIN के साथ अपने वॉलेट तक पहुंच पुनः प्राप्त करें।", "learn_how_to_withdraw_crypto_from_exchange": "जानें कि इन एक्सचेंजों से क्रिप्टो संपत्तियाँ OneKey में कैसे निकाली जाएँ", "learn_more_about_qr_code_wallet": "QR-based वॉलेट के बारे में और जानें", "lightning_invoice": "लाइटनिंग इनवॉइस", @@ -2044,6 +2060,7 @@ "market.ath_desc": "{token} का सर्वकालिक उच्च स्तर {time} को था, {price} पर, और वर्तमान मूल्य उस उच्च स्तर से {percent} नीचे है।", "market.atl_desc": "{token} का सर्वकालिक निम्नतम मूल्य {time} को {price} पर था, और वर्तमान मूल्य उस निम्नतम मूल्य से {percent} ऊपर है।", "market.cex": "CEX", + "market.change_24h": "परिवर्तन / 24h", "market.chart": "चार्ट", "market.days_since_launch": "लॉन्च के बाद से दिन", "market.empty_watchlist_desc": "अपने पसंदीदा टोकनों को वॉचलिस्ट में जोड़ें", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "चयनित खाते के लिए अनुपलब्ध नेटवर्क", "network_show_enabled_only": "केवल सक्षम नेटवर्क दिखाएं", "network_visible_in_all_network_tooltip_title": "'सभी नेटवर्क' दृश्य में दिखाया गया", + "new_pin_created": "नया पिन बनाया गया", + "new_pin_created_desc": "आप सेटिंग्स के माध्यम से किसी भी समय अपना पिन बदल सकते हैं।", "nft.already_collected": "यह NFT पहले ही संग्रहित कर लिया गया है।", "nft.attributes": "गुण", "nft.collect_failed": "NFT संग्रह विफल रहा, कृपया पुनः प्रयास करें।", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "साइड पैनल के रूप में खोलें", "open_in_mobile_app": "मोबाइल ऐप में खोलें", "open_ordinals_transfer_tutorial_url_message": "Ordinals संपत्तियों को कैसे स्थानांतरित करें?", + "open_source_secure_sharding": "ओपन सोर्स सुरक्षित शेडिंग", "p2pkh_desc": "\"1\" के साथ शुरू होता है। P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "\"bc1q\" से शुरू होता है। P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "\"bc1p\" से शुरू होता है। P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "और चाहिए {token}?", "perps.get_reward": "पुरस्कार पाना", "perps.offline_moblie": "कनेक्शन खो गया। कृपया अपना नेटवर्क जांचें और पुल-टू-रिफ्रेश करने का प्रयास करें।", + "perps.settings_return_to_default_layout": "डिफ़ॉल्ट लेआउट पर लौटें", "perps.share_position_background": "पृष्ठभूमि", "perps.share_position_btn_Share_on_x": "X पर साझा करें", "perps.share_position_btn_copy_link": "लिंक कॉपी करें", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "शेयरों", "perps.trade_reward": "व्यापार पुरस्कार", "pick_your_device": "अपना डिवाइस चुनें", + "pin_attempts_cooldown": "{seconds} में पुनः प्रयास करें।", + "pin_attempts_remaining": "गलत पिन दर्ज किया गया। ${attemptsRemaining} प्रयास शेष हैं।", "preparing_backup_desc": "बस एक पल…", "preparing_backup_title": "बैकअप तैयार किया जा रहा है…", "prime.about_cloud_sync": "क्लाउड सिंक के बारे में", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "अपने वॉलेट पते का उपयोग करके प्राप्त करें", "receive_from_exchange": "एक्सचेंज से प्राप्त करें", "receive_token_list_footer_text": "टोकन नहीं मिल रहा? खोजने का प्रयास करें", + "recovery_phrase_free": "रिकवरी फ़्रेज़ मुक्त", "recovery_phrase_screenshot_protected_desc": "आपकी संपत्ति की सुरक्षा के लिए, आपकी रिकवरी वाक्यांश कभी भी स्क्रीनशॉट्स में दिखाई नहीं देगा।", "recovery_phrase_screenshot_protected_title": "रिकवरी वाक्यांश सुरक्षित है", "referral.accept": "स्वीकार करना", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} पते", "referral_code_tutorial_label": "रेफ़रल कोड कैसे प्राप्त करें?", "referral_promo_title": "OneKey रेफ़रल प्रोग्राम से जुड़ें", + "remember_your_pin": "अपना पिन याद है?", + "remember_your_pin_desc": "बस एक दोस्ताना अनुस्मारक कि अपने पिन को याद रखें। हम समय-समय पर जांच करते रहेंगे।", "remove_account_desc": "यह खाता हटा दिया जाएगा।", "remove_device": "उपकरण हटाएं", "remove_device_desc": "यह इस हार्डवेयर वॉलेट को OneKey App से डिस्कनेक्ट कर देगा। आप इसे जब चाहें App से फिर से कनेक्ट कर सकते हैं।", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "सुनिश्चित करें कि आपने वॉलेट को हटाने से पहले रिकवरी फ़्रेज़ लिख लिया है। अन्यथा, आप वॉलेट को पुनः प्राप्त नहीं कर पाएंगे।", "remove_wallet_double_confirm_message": "मैंने रिकवरी फ़्रेज़ लिख लिया है", "reset_app_desc": "यह OneKey पर आपके द्वारा बनाए गए सभी डेटा को हटा देगा। सुनिश्चित करने के बाद कि आपके पास उचित बैकअप है, ऐप को रीसेट करने के लिए \"RESET\" दर्ज करें।", + "reset_pin": "पिन रीसेट करें", "scan.camera_access_denied": "कैमरा पहुंच अस्वीकृत", "scan.enable_camera_permissions": "OneKey को QR कोड स्कैन करने के लिए कैमरा की आवश्यकता होती है। कृपया \"सेटिंग्स\" पर जाएं और इस सुविधा का उपयोग करने के लिए कैमरा अनुमतियां सक्षम करें।", "scan.grant_camera_access_in_expand_view": "कृपया विस्तार दृश्य में कैमरा पहुंच प्रदान करें।", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "पता बनाने के लिए स्कैन करें", "scanning_text": "स्कैनिंग", "secure_qr_toast_scan_qr_code_on_device_text": "जब QR कोड दिखाई दे, तो 'अगला' पर क्लिक करें, फिर उसे स्कैन करें।", + "seed_phrase_wallet": "सीड फ्रेज़ वॉलेट", "select_connect_app_on_home": "होम स्क्रीन पर Connect App Wallet चुनें", "select_onekey_app": "OneKey App चुनें", "select_recovery_phrase_length": "एक लंबाई चुनें", + "select_your_email": "अपना ईमेल चुनें", + "select_your_email_desc": "अपने Google या Apple खाते के साथ वॉलेट जोड़ें", "selected_network": "चयनित नेटवर्क", "selected_network_only_supports_device": "चयनित नेटवर्क वर्तमान में केवल {deviceType} का समर्थन करता है।", "self_troubleshooting": "स्व-समस्या निवारण", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "स्वीकृत कर रहा है...", "swap.btn_building": "ऑर्डर बनाना...", "swap.ch_status_hold": "समर्थन से संपर्क करें", + "swap.current_token": "वर्तमान टोकन", "swap.limit_amount": "{num1} {fromToken} को {num2} {toToken} में बेचें", "swap.loading_content": "सबसे अच्छी कीमत की गणना कर रहे हैं...", "swap.native_token_max_tip": "कृपया नेटवर्क शुल्क का भुगतान करने के लिए थोड़ी मात्रा में टोकन सुरक्षित रखें, अन्यथा लेनदेन विफल हो जाएगा।", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "सेवा {accelerator} द्वारा प्रदान की गई", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "टिप्पणी", "tx_accelerate.speed_up_with_accelerator_dialog_title": "{accelerator} के साथ गति बढ़ाएं", + "ultra_fast_setup": "अति-तीव्र सेटअप", "update.all_other_apps_closed": "अन्य सभी OneKey ऐप्स और वेब अपग्रेड उपकरण बंद हैं।", "update.all_other_apps_closed_emoji": "अन्य सभी OneKey ऐप्स और वेब अपग्रेड उपकरण बंद हैं। 🆗", "update.all_updates_complete": "सभी अपडेट पूरे हो गए 👏🏻", diff --git a/packages/shared/src/locale/json/id.json b/packages/shared/src/locale/json/id.json index 8b99f21a1b4c..224ac1a1180c 100644 --- a/packages/shared/src/locale/json/id.json +++ b/packages/shared/src/locale/json/id.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "Sewa Energi", "bandwidth_energy.title": "Bandwidth & Energi", "bandwidth_energy.what_is_bandwidth_energy": "Apa itu Bandwidth & Energy pada Tron?", + "beginner_friendly": "Ramah untuk pemula", "bip44__standard": "Standar BIP44", "bluetooth.disable_in_settings": "Jika Anda lebih suka hanya menggunakan USB dan tidak ingin melihat prompt ini lagi, buka aplikasi OneKey > Pengaturan > Preferensi dan matikan Bluetooth.", "bluetooth.disabled": "Bluetooth dinonaktifkan", @@ -294,6 +295,8 @@ "communication.timeout": "Waktu komunikasi habis", "confirm_exit_dialog_desc": "Anda yakin ingin keluar dari proses migrasi data?", "confirm_exit_dialog_title": "Konfirmasi keluar", + "confirm_your_pin": "Konfirmasi PIN Anda", + "confirm_your_pin_desc": "Jika Anda lupa PIN ini, Anda tidak akan dapat memulihkan dompet Anda di perangkat baru.", "connect_device_to_computer_via_usb": "Sambungkan {deviceLabel} ke komputer Anda melalui USB", "connect_with_qr_code": "Hubungkan dengan Kode QR", "contact_us_instruction": "Butuh bantuan lebih lanjut?", @@ -306,6 +309,7 @@ "content__normal": "Normal", "content__slow": "Lambat", "content__to": "Ke", + "continue_with_social_platform": "Lanjutkan dengan {platform}", "copy_address_modal_item_create_address_instruction": "Buat alamat", "copy_address_modal_title": "Alamat akun", "copy_anyway": "Salin saja", @@ -314,6 +318,8 @@ "count_assets": "{count} aset", "count_hidden_assets": "{count} aset tersembunyi", "count_words": "{length} kata", + "create_a_pin": "Buat PIN", + "create_a_pin_desc": "Ini digunakan untuk mengamankan dompet Anda di semua perangkat Anda. Ini tidak dapat dipulihkan.", "create_new_wallet_badge_consists": "Frasa pemulihan terdiri dari 12 kata", "create_new_wallet_badge_handwritten": "Cadangan tulisan tangan", "create_new_wallet_badge_keep": "Harus menjaga keamanannya sendiri", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "Paling sering digunakan", "create_new_wallet_badge_supports": "Mendukung ratusan jaringan", "create_new_wallet_learn_more": "Frasa pemulihan adalah inti dari keamanan dompet Anda. Frasa ini terdiri dari 12 kata bahasa Inggris umum yang digunakan untuk membuat dan memulihkan kunci privat serta alamat dompet Anda. Tulislah dengan tangan dan simpan dengan aman — hanya Anda yang memiliki akses ke aset Anda.", + "create_passcode_desc": "Anda akan menggunakan ini untuk membuka kunci dompet Anda.", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "Setelah dompet standar dibuat, masukkan passphrase untuk membuat dompet tersembunyi.", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "Buat dompet tersembunyi", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "Tidak ada Passphrase yang diperlukan sebelum menampilkan kode QR. Ketuk tombol ✅, tunjukkan kode tersebut, dan pindai dengan aplikasi untuk membuat dompet standar berbasis QR.", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "Kode PIN tidak valid", "enter_pin.title": "Masukkan kode PIN", "enter_pin_on_app": "Masukkan Pin di aplikasi", + "enter_your_pin": "Masukkan PIN Anda", + "enter_your_pin_desc": "Email ini sudah memiliki dompet yang dibuat. Silakan masukkan PIN Anda untuk masuk.", "explore.add_bookmark": "Tambah bookmark", "explore.add_to_whitelist": "tambahkan ke daftar putih", "explore.addresses_count": "{number} alamat", @@ -1072,6 +1081,7 @@ "for_large_assets": "Untuk aset besar", "for_reference_only": "Hanya untuk referensi", "forgot_password_no_question_mark": "Lupa kata sandi", + "forgot_pin": "Lupa PIN?", "form.address_error_invalid": "Alamat tidak valid", "form.address_placeholder": "Alamat atau domain", "form.amount_placeholder": "Masukkan jumlah", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "Unduh bridge komunikasi", "global.faqs_firmware_detection": "Uji Koneksi", "global.faqs_forgot_pin": "Lupa PIN", - "global.faqs_reset_wallet": "Reset wallet", + "global.faqs_reset_device": "Setel ulang perangkat", "global.favorites": "Favorit", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "Cari nama akun", "global.search_address": "Cari alamat", "global.search_asset": "Cari aset", + "global.search_everything": "Cari semuanya", "global.search_no_results_desc": "Coba ubah kata kunci pencarian", "global.search_no_results_title": "Tidak ada hasil", + "global.search_placeholder_web": "Cari", "global.search_tokens": "Cari token", "global.secure_install": "Instalasi aman", "global.security": "Keamanan", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "Dompet Pantau", "global.watched": "Ditonton", "global.watchlist": "Daftar Pantauan", + "global.web_access_for_hardware_wallet_disconnected": "Akses web untuk koneksi dompet perangkat keras telah dihentikan. Perangkat yang terhubung masih dapat digunakan; jika Anda mengalami masalah, silakan gunakan App atau ekstensi browser.", "global.web_feature_not_available_go_to_app": "Fitur ini tidak tersedia di web. Silakan gunakan aplikasi desktop atau seluler.", "global.website": "Situs web", "global.what_happen": "Yang akan terjadi:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "Silakan masukkan kata sandi untuk cadangan ini.", "import_hardware_phrases_warning": "Jangan impor frasa pemulihan dompet perangkat keras Anda. Sambungkan Dompet Perangkat Keras ↗ sebagai gantinya", "import_phrase_or_private_key": "Impor frasa atau kunci privat", + "incorrect_pin": "PIN salah. Silakan coba lagi.", "insufficient_fee_append_desc": "berdasarkan perkiraan biaya maksimum: {amount} {symbol}", "interact_with_contract": "Berinteraksi dengan (Kepada)", "kaspa_official": "Kaspa Resmi", + "keyless_wallet": "Dompet tanpa kunci", + "keyless_wallet_desc": "Dompet tanpa kunci Anda disimpan dengan aman dan terdesentralisasi di berbagai faktor. Pulihkan akses ke dompet Anda dengan akun Google atau Apple dan PIN 4 digit.", "learn_how_to_withdraw_crypto_from_exchange": "Pelajari cara menarik aset kripto dari bursa ini ke OneKey", "learn_more_about_qr_code_wallet": "Pelajari lebih lanjut tentang dompet QR-based", "lightning_invoice": "Invoice Kilat", @@ -2044,6 +2060,7 @@ "market.ath_desc": "Harga tertinggi sepanjang masa {token} adalah pada {time}, di {price}, dan harga saat ini turun sebesar {percent} dari harga tertinggi tersebut.", "market.atl_desc": "Harga terendah sepanjang masa {token} terjadi pada {time} , pada {price} , dan harga saat ini naik sebesar {percent} dari harga terendah tersebut.", "market.cex": "CEX", + "market.change_24h": "Perubahan / 24h", "market.chart": "Bagan", "market.days_since_launch": "Hari sejak peluncuran", "market.empty_watchlist_desc": "Tambahkan token favorit Anda ke daftar pantauan", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "Jaringan tidak tersedia untuk akun yang dipilih", "network_show_enabled_only": "Hanya tampilkan jaringan yang diaktifkan", "network_visible_in_all_network_tooltip_title": "Ditampilkan dalam tampilan 'Semua jaringan'", + "new_pin_created": "PIN Baru Dibuat", + "new_pin_created_desc": "Anda dapat mengubah PIN Anda kapan saja melalui Pengaturan.", "nft.already_collected": "NFT ini sudah dikumpulkan.", "nft.attributes": "Atribut", "nft.collect_failed": "Pengumpulan NFT gagal, silakan coba lagi.", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "Buka sebagai panel samping", "open_in_mobile_app": "Buka di Aplikasi Seluler", "open_ordinals_transfer_tutorial_url_message": "Bagaimana cara mentransfer aset Ordinals?", + "open_source_secure_sharding": "Shading aman sumber terbuka", "p2pkh_desc": "Mulai dengan \"1\". P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "Mulai dengan \"bc1q\". P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "Mulai dengan \"bc1p\". P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "Butuh lebih banyak {token}?", "perps.get_reward": "Dapatkan Hadiah", "perps.offline_moblie": "Koneksi terputus. Silakan periksa jaringan Anda dan coba tarik untuk menyegarkan.", + "perps.settings_return_to_default_layout": "Kembali ke tata letak default", "perps.share_position_background": "Latar Belakang", "perps.share_position_btn_Share_on_x": "Bagikan di X", "perps.share_position_btn_copy_link": "Salin Tautan", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "Saham", "perps.trade_reward": "Hadiah perdagangan", "pick_your_device": "Pilih perangkat Anda", + "pin_attempts_cooldown": "Coba lagi dalam {seconds}.", + "pin_attempts_remaining": "PIN yang dimasukkan salah. {attemptsRemaining} percobaan tersisa.", "preparing_backup_desc": "Tunggu sebentar…", "preparing_backup_title": "Menyiapkan cadangan…", "prime.about_cloud_sync": "Tentang Cloud Sync", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "Terima menggunakan alamat dompet Anda", "receive_from_exchange": "Terima dari bursa", "receive_token_list_footer_text": "Tidak dapat menemukan token? Coba cari", + "recovery_phrase_free": "Bebas frasa pemulihan", "recovery_phrase_screenshot_protected_desc": "Demi keamanan aset Anda, frasa pemulihan Anda tidak akan pernah muncul dalam tangkapan layar.", "recovery_phrase_screenshot_protected_title": "Frasa pemulihan terlindungi", "referral.accept": "Menerima", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} alamat", "referral_code_tutorial_label": "Cara mendapatkan kode rujukan?", "referral_promo_title": "Bergabung dengan Program Rujukan OneKey", + "remember_your_pin": "Ingat PIN Anda?", + "remember_your_pin_desc": "Hanya pengingat ramah untuk menjaga PIN Anda tetap segar dalam ingatan. Kami akan memeriksa dari waktu ke waktu.", "remove_account_desc": "Akun ini akan dihapus.", "remove_device": "Hapus perangkat", "remove_device_desc": "Ini akan memutuskan dompet hardware ini dari OneKey App. Anda dapat menyambungkannya kembali ke App kapan saja Anda mau.", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "Pastikan Anda telah menulis frase pemulihan sebelum menghapus dompet. Jika tidak, Anda tidak akan dapat memulihkan dompet tersebut.", "remove_wallet_double_confirm_message": "Saya telah menulis frase pemulihan", "reset_app_desc": "Ini akan menghapus semua data yang telah Anda buat di OneKey. Setelah memastikan bahwa Anda memiliki cadangan yang tepat, masukkan \"RESET\" untuk mereset Aplikasi", + "reset_pin": "Atur Ulang PIN", "scan.camera_access_denied": "Akses kamera ditolak", "scan.enable_camera_permissions": "OneKey memerlukan akses kamera untuk memindai kode QR. Silakan pergi ke \"Pengaturan\" dan aktifkan izin kamera untuk menggunakan fitur ini.", "scan.grant_camera_access_in_expand_view": "Silakan berikan akses kamera dalam tampilan perluas.", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "Pindai untuk membuat alamat", "scanning_text": "Memindai", "secure_qr_toast_scan_qr_code_on_device_text": "Kembali saat kode QR muncul, klik 'Berikutnya', lalu pindai.", + "seed_phrase_wallet": "Dompet frasa seed", "select_connect_app_on_home": "Pilih Connect App Wallet di layar beranda", "select_onekey_app": "Pilih App OneKey", "select_recovery_phrase_length": "Pilih panjang", + "select_your_email": "Pilih email Anda", + "select_your_email_desc": "Tambahkan dompet dengan akun Google atau Apple Anda", "selected_network": "Jaringan terpilih", "selected_network_only_supports_device": "Jaringan yang dipilih saat ini hanya mendukung {deviceType}", "self_troubleshooting": "Pemecahan Masalah Mandiri", @@ -3283,7 +3313,7 @@ "settings.upload_state_logs": "Unggah log status", "settings.uploading_logs_progress": "Mengunggah log… {progress}%", "settings.user_agreement": "Perjanjian pengguna", - "settings.version_versionnum": "versi {versionNum}", + "settings.version_versionnum": "Versi {versionNum}", "settings.view_address_in_explorer": "Lihat alamat di penjelajah", "settings.view_transaction_in_explorer": "Lihat transaksi di penjelajah", "settings.whats_new": "Apa yang baru", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "Menyetujui...", "swap.btn_building": "Membangun pesanan...", "swap.ch_status_hold": "Hubungi dukungan", + "swap.current_token": "Token saat ini", "swap.limit_amount": "Jual {num1} {fromToken} untuk {num2} {toToken}", "swap.loading_content": "Menghitung harga terbaik...", "swap.native_token_max_tip": "Harap sisakan sejumlah kecil token untuk membayar biaya jaringan, jika tidak transaksi akan gagal.", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "Layanan disediakan oleh {accelerator}", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "Catatan", "tx_accelerate.speed_up_with_accelerator_dialog_title": "Percepat dengan {accelerator}", + "ultra_fast_setup": "Pengaturan sangat cepat", "update.all_other_apps_closed": "Semua aplikasi OneKey lainnya dan alat peningkatan web ditutup.", "update.all_other_apps_closed_emoji": "Semua Aplikasi OneKey lainnya dan alat peningkatan web ditutup. 🆗", "update.all_updates_complete": "Semua pembaruan selesai 👏🏻", diff --git a/packages/shared/src/locale/json/it_IT.json b/packages/shared/src/locale/json/it_IT.json index a6422541374c..5da17dd2408a 100644 --- a/packages/shared/src/locale/json/it_IT.json +++ b/packages/shared/src/locale/json/it_IT.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "Affitta Energia", "bandwidth_energy.title": "Larghezza di banda & Energia", "bandwidth_energy.what_is_bandwidth_energy": "Cosa sono la Larghezza di banda & l'Energia su Tron?", + "beginner_friendly": "Adatto ai principianti", "bip44__standard": "Standard BIP44", "bluetooth.disable_in_settings": "Se preferisci utilizzare solo USB e non vuoi vedere più questo messaggio, vai nell'app OneKey > Impostazioni > Preferenze e disattiva il Bluetooth.", "bluetooth.disabled": "Il Bluetooth è disabilitato", @@ -294,6 +295,8 @@ "communication.timeout": "Timeout di comunicazione", "confirm_exit_dialog_desc": "Sei sicuro di voler uscire dal processo di migrazione dei dati?", "confirm_exit_dialog_title": "Conferma uscita", + "confirm_your_pin": "Conferma il tuo PIN", + "confirm_your_pin_desc": "Se dimentichi questo PIN, non potrai recuperare il tuo portafoglio su un nuovo dispositivo.", "connect_device_to_computer_via_usb": "Collega {deviceLabel} al tuo computer tramite USB", "connect_with_qr_code": "Connetti con codice QR", "contact_us_instruction": "Hai bisogno di ulteriore aiuto?", @@ -306,6 +309,7 @@ "content__normal": "Normale", "content__slow": "Lento", "content__to": "A", + "continue_with_social_platform": "Continua con {platform}", "copy_address_modal_item_create_address_instruction": "Crea indirizzo", "copy_address_modal_title": "Indirizzo del conto", "copy_anyway": "Copia comunque", @@ -314,6 +318,8 @@ "count_assets": "{count} risorse", "count_hidden_assets": "{count} asset nascosti", "count_words": "{length} parole", + "create_a_pin": "Crea un PIN", + "create_a_pin_desc": "Questo viene utilizzato per proteggere il tuo portafoglio su tutti i tuoi dispositivi. Non può essere recuperato.", "create_new_wallet_badge_consists": "La frase di recupero è composta da 12 parole", "create_new_wallet_badge_handwritten": "Backup scritto a mano", "create_new_wallet_badge_keep": "Devi tenerlo al sicuro da solo", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "Più utilizzate", "create_new_wallet_badge_supports": "Supporta centinaia di reti", "create_new_wallet_learn_more": "La frase di recupero è il fulcro della sicurezza del tuo wallet. È composta da 12 comuni parole inglesi utilizzate per creare e ripristinare la tua chiave privata e l’indirizzo del wallet. Trascrivila a mano e conservala in un luogo sicuro — solo tu hai accesso ai tuoi asset.", + "create_passcode_desc": "Lo userai per sbloccare il tuo portafoglio.", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "Dopo la creazione del portafoglio standard, inserisci una passphrase per creare un portafoglio nascosto.", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "Crea portafoglio nascosto", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "Non è necessario alcun Passphrase prima di visualizzare il codice QR. Tocca il pulsante ✅, mostra il codice e scansionalo con l'app per creare un portafoglio standard basato su QR.", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "Codice PIN non valido", "enter_pin.title": "Inserisci il codice PIN", "enter_pin_on_app": "Inserisci il Pin sull'app", + "enter_your_pin": "Inserisci il tuo PIN", + "enter_your_pin_desc": "Questa email ha già un portafoglio creato. Inserisci il tuo PIN per accedere.", "explore.add_bookmark": "Aggiungi segnalibro", "explore.add_to_whitelist": "aggiungi alla whitelist", "explore.addresses_count": "{number} indirizzi", @@ -1072,6 +1081,7 @@ "for_large_assets": "Per asset di grandi dimensioni", "for_reference_only": "Solo per riferimento", "forgot_password_no_question_mark": "Password dimenticata", + "forgot_pin": "PIN dimenticato?", "form.address_error_invalid": "Indirizzo non valido", "form.address_placeholder": "Indirizzo o dominio", "form.amount_placeholder": "Inserisci importo", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "Scarica il bridge di comunicazione", "global.faqs_firmware_detection": "Test connessione", "global.faqs_forgot_pin": "PIN dimenticato", - "global.faqs_reset_wallet": "Reimposta wallet", + "global.faqs_reset_device": "Ripristina dispositivo", "global.favorites": "Preferiti", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "Cerca nome account", "global.search_address": "Cerca indirizzo", "global.search_asset": "Cerca asset", + "global.search_everything": "Cerca tutto", "global.search_no_results_desc": "Prova a cambiare la parola chiave di ricerca", "global.search_no_results_title": "Nessun risultato", + "global.search_placeholder_web": "Cerca", "global.search_tokens": "Cerca token", "global.secure_install": "Installazione sicura", "global.security": "Sicurezza", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "Portafoglio di sola visualizzazione", "global.watched": "Visto", "global.watchlist": "Lista di controllo", + "global.web_access_for_hardware_wallet_disconnected": "L'accesso web per la connessione del portafoglio hardware è stato interrotto. I dispositivi connessi possono ancora essere utilizzati; in caso di problemi, utilizza l'App o l'estensione del browser.", "global.web_feature_not_available_go_to_app": "Questa funzione non è disponibile sul web. Si prega di utilizzare l'app desktop o mobile.", "global.website": "Sito web", "global.what_happen": "Cosa accadrà:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "Inserisci la password per questo backup.", "import_hardware_phrases_warning": "Non importare la frase di recupero del tuo hardware wallet. Collega hardware wallet ↗ invece", "import_phrase_or_private_key": "Importa frase o chiave privata", + "incorrect_pin": "PIN errato. Riprova.", "insufficient_fee_append_desc": "basato sulla tariffa massima stimata: {amount} {symbol}", "interact_with_contract": "Interagisci con (A)", "kaspa_official": "Kaspa ufficiale", + "keyless_wallet": "Portafoglio senza chiave", + "keyless_wallet_desc": "Il tuo portafoglio senza chiavi è archiviato in modo sicuro e decentralizzato su più fattori. Recupera l'accesso al tuo portafoglio con il tuo account Google o Apple e un PIN a 4 cifre.", "learn_how_to_withdraw_crypto_from_exchange": "Scopri come prelevare asset crypto da questi exchange su OneKey", "learn_more_about_qr_code_wallet": "Scopri di più sul portafoglio QR-based", "lightning_invoice": "Fattura Lightning", @@ -2044,6 +2060,7 @@ "market.ath_desc": "Il massimo storico di {token} è stato il {time}, a {price}, e il prezzo attuale è diminuito del {percent} rispetto a quel massimo.", "market.atl_desc": "Il minimo storico di {token} è stato raggiunto {time} , a {price} , e il prezzo attuale è aumentato del {percent} rispetto a quel minimo.", "market.cex": "CEX", + "market.change_24h": "Variazione / 24h", "market.chart": "Grafico", "market.days_since_launch": "Giorni dal lancio", "market.empty_watchlist_desc": "Aggiungi i tuoi token preferiti alla watchlist", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "Reti non disponibili per l'account selezionato", "network_show_enabled_only": "Mostra solo le reti abilitate", "network_visible_in_all_network_tooltip_title": "Mostrato nella visualizzazione 'Tutte le reti'", + "new_pin_created": "Nuovo PIN creato", + "new_pin_created_desc": "Puoi modificare il tuo PIN in qualsiasi momento tramite Impostazioni.", "nft.already_collected": "Questo NFT è già stato raccolto.", "nft.attributes": "Attributi", "nft.collect_failed": "La raccolta di NFT è fallita, per favore prova ancora.", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "Aperto come pannello laterale", "open_in_mobile_app": "Apri nell'app mobile", "open_ordinals_transfer_tutorial_url_message": "Come trasferire gli assets Ordinals?", + "open_source_secure_sharding": "Ombreggiatura sicura open source", "p2pkh_desc": "Inizia con \"1\". P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "Inizia con \"bc1q\". P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "Inizia con \"bc1p\". P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "Ne ho bisogno di più {token}?", "perps.get_reward": "Ottieni ricompense", "perps.offline_moblie": "Connessione persa. Controlla la tua rete e prova a trascinare per aggiornare.", + "perps.settings_return_to_default_layout": "Torna al layout predefinito", "perps.share_position_background": "Sfondo", "perps.share_position_btn_Share_on_x": "Condividi su X", "perps.share_position_btn_copy_link": "Copia link", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "Azioni", "perps.trade_reward": "Ricompensa commerciale", "pick_your_device": "Scegli il tuo dispositivo", + "pin_attempts_cooldown": "Riprova tra {seconds}.", + "pin_attempts_remaining": "PIN errato. ${attemptsRemaining} tentativi rimanenti.", "preparing_backup_desc": "Un momento…", "preparing_backup_title": "Preparazione del backup…", "prime.about_cloud_sync": "Info sulla sincronizzazione cloud", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "Ricevi usando il tuo indirizzo del wallet", "receive_from_exchange": "Ricevi dall’exchange", "receive_token_list_footer_text": "Non riesci a trovare il token? Prova a cercare", + "recovery_phrase_free": "Senza frase di recupero", "recovery_phrase_screenshot_protected_desc": "Per la sicurezza dei tuoi asset, la tua frase di recupero non apparirà mai negli screenshot.", "recovery_phrase_screenshot_protected_title": "Frase di recupero protetta", "referral.accept": "Accettare", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} indirizzi", "referral_code_tutorial_label": "Come ottenere un codice di referral?", "referral_promo_title": "Partecipa al programma di referral OneKey", + "remember_your_pin": "Ricordi il tuo PIN?", + "remember_your_pin_desc": "Solo un promemoria amichevole per tenere il tuo PIN sempre a mente. Controlleremo di tanto in tanto.", "remove_account_desc": "Questo account verrà rimosso.", "remove_device": "Rimuovi dispositivo", "remove_device_desc": "Questo disconnetterà questo portafoglio hardware dall'App OneKey. Puoi riconnetterlo all'App quando vuoi.", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "Assicurati di aver annotato la frase di recupero prima di rimuovere il portafoglio. Altrimenti, non sarai in grado di recuperare il portafoglio.", "remove_wallet_double_confirm_message": "Ho annotato la frase di recupero", "reset_app_desc": "Questo cancellerà tutti i dati che hai creato su OneKey. Dopo esserti assicurato di avere un backup adeguato, inserisci \"RESET\" per ripristinare l'App", + "reset_pin": "Reimposta PIN", "scan.camera_access_denied": "Accesso alla fotocamera negato", "scan.enable_camera_permissions": "OneKey richiede l'accesso alla fotocamera per scansionare i codici QR. Si prega di andare su \"Impostazioni\" e abilitare i permessi della fotocamera per utilizzare questa funzione.", "scan.grant_camera_access_in_expand_view": "Concedi l'accesso alla fotocamera nella vista espansa.", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "Scansiona per creare un indirizzo", "scanning_text": "Scansione", "secure_qr_toast_scan_qr_code_on_device_text": "Torna indietro quando appare il codice QR, clicca su 'Avanti', poi scansionalo.", + "seed_phrase_wallet": "Portafoglio con frase di recupero", "select_connect_app_on_home": "Seleziona Connect App Wallet nella schermata principale", "select_onekey_app": "Seleziona App OneKey", "select_recovery_phrase_length": "Seleziona una lunghezza", + "select_your_email": "Seleziona la tua email", + "select_your_email_desc": "Aggiungi un portafoglio con il tuo account Google o Apple", "selected_network": "Rete selezionata", "selected_network_only_supports_device": "La rete selezionata attualmente supporta solo {deviceType}", "self_troubleshooting": "Risoluzione autonoma dei problemi", @@ -3283,7 +3313,7 @@ "settings.upload_state_logs": "Carica i log di stato", "settings.uploading_logs_progress": "Caricamento dei log… {progress}%", "settings.user_agreement": "Accordo utente", - "settings.version_versionnum": "versione {versionNum}", + "settings.version_versionnum": "Versione {versionNum}", "settings.view_address_in_explorer": "Visualizza indirizzo in esplora risorse", "settings.view_transaction_in_explorer": "Visualizza transazione nell'explorer", "settings.whats_new": "Cosa c'è di nuovo", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "Approvazione in corso...", "swap.btn_building": "Costruzione dell'ordine...", "swap.ch_status_hold": "Contatta l'assistenza", + "swap.current_token": "Token attuali", "swap.limit_amount": "Vendi {num1} {fromToken} per {num2} {toToken}", "swap.loading_content": "Calcolando il miglior prezzo...", "swap.native_token_max_tip": "Si prega di riservare una piccola quantità di token per pagare le commissioni di rete, altrimenti la transazione non andrà a buon fine.", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "Servizio fornito da {accelerator}", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "Nota", "tx_accelerate.speed_up_with_accelerator_dialog_title": "Accelera con {accelerator}", + "ultra_fast_setup": "Configurazione ultra-rapida", "update.all_other_apps_closed": "Tutte le altre App OneKey e gli strumenti di aggiornamento web sono chiusi.", "update.all_other_apps_closed_emoji": "Tutte le altre app OneKey e gli strumenti di aggiornamento web sono chiusi. 🆗", "update.all_updates_complete": "Tutti gli aggiornamenti completati 👏🏻", diff --git a/packages/shared/src/locale/json/ja_JP.json b/packages/shared/src/locale/json/ja_JP.json index 6a089314b7dd..bab0412c6f2d 100644 --- a/packages/shared/src/locale/json/ja_JP.json +++ b/packages/shared/src/locale/json/ja_JP.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "レントエネルギー", "bandwidth_energy.title": "帯域幅 & エネルギー", "bandwidth_energy.what_is_bandwidth_energy": "Tronの帯域幅とエネルギーとは何ですか?", + "beginner_friendly": "初心者向け", "bip44__standard": "BIP44規格", "bluetooth.disable_in_settings": "USBのみを使用したい場合、今後このプロンプトを表示したくない場合は、OneKeyアプリ > 設定 > 詳細設定でBluetoothをオフにしてください。", "bluetooth.disabled": "Bluetooth が無効になっています", @@ -294,6 +295,8 @@ "communication.timeout": "通信タイムアウト", "confirm_exit_dialog_desc": "データ移行プロセスを終了してもよろしいですか?", "confirm_exit_dialog_title": "終了を確認", + "confirm_your_pin": "PINを確認してください", + "confirm_your_pin_desc": "このPINを忘れると、新しいデバイスでウォレットを復元できなくなります。", "connect_device_to_computer_via_usb": "USB経由で{deviceLabel}をコンピュータに接続してください", "connect_with_qr_code": "QRコードで接続", "contact_us_instruction": "さらにサポートが必要ですか?", @@ -306,6 +309,7 @@ "content__normal": "ノーマル", "content__slow": "遅い", "content__to": "宛先", + "continue_with_social_platform": "{platform}で続ける", "copy_address_modal_item_create_address_instruction": "住所を作成", "copy_address_modal_title": "アカウントアドレス", "copy_anyway": "それでもコピーする", @@ -314,6 +318,8 @@ "count_assets": "{count} アセット", "count_hidden_assets": "{count} 隠された資産", "count_words": "{length} 語", + "create_a_pin": "PINを作成", + "create_a_pin_desc": "これは、すべてのデバイスでウォレットを保護するために使用されます。これは復元できません。", "create_new_wallet_badge_consists": "リカバリーフレーズは12語で構成されています", "create_new_wallet_badge_handwritten": "手書きのバックアップ", "create_new_wallet_badge_keep": "自分で安全に保管する必要があります", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "最も使用されている", "create_new_wallet_badge_supports": "数百のネットワークをサポート", "create_new_wallet_learn_more": "リカバリーフレーズは、ウォレットのセキュリティの中核です。これは12個の一般的な英単語で構成されており、あなたの秘密鍵やウォレットアドレスを作成・復元するために使われます。手書きで控えて安全に保管してください——あなたの資産にアクセスできるのはあなただけです。", + "create_passcode_desc": "これを使用してウォレットのロックを解除します。", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "標準ウォレットが作成された後、隠しウォレットを作るためにパスフレーズを入力してください。", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "隠しウォレットを作成する", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "QRコードを表示する前に、passphraseは必要ありません。✅ボタンをタップし、コードを表示してアプリでスキャンすることで、QR-basedの標準ウォレットを作成します。", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "無効なPINコード", "enter_pin.title": "PINコードを入力してください", "enter_pin_on_app": "アプリでPINを入力してください", + "enter_your_pin": "PINを入力してください", + "enter_your_pin_desc": "このメールアドレスには既にウォレットが作成されています。ログインするにはPINを入力してください。", "explore.add_bookmark": "ブックマークを追加", "explore.add_to_whitelist": "ホワイトリストに追加", "explore.addresses_count": "{number} 件のアドレス", @@ -1072,6 +1081,7 @@ "for_large_assets": "大規模な資産向け", "for_reference_only": "参考のためだけ", "forgot_password_no_question_mark": "パスワードをお忘れですか", + "forgot_pin": "PINをお忘れですか?", "form.address_error_invalid": "無効なアドレス", "form.address_placeholder": "アドレスまたはドメイン", "form.amount_placeholder": "金額を入力してください", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "ハードウェアブリッジをダウンロード", "global.faqs_firmware_detection": "ファームウェア検出", "global.faqs_forgot_pin": "PINを忘れました", - "global.faqs_reset_wallet": "ウォレットをリセット", + "global.faqs_reset_device": "デバイスをリセット", "global.favorites": "お気に入り", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "アカウント名を検索", "global.search_address": "住所を検索", "global.search_asset": "アセットを検索", + "global.search_everything": "すべて検索", "global.search_no_results_desc": "検索キーワードを変更してみてください", "global.search_no_results_title": "結果なし", + "global.search_placeholder_web": "検索", "global.search_tokens": "トークンを検索", "global.secure_install": "安全なインストール", "global.security": "安全", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "閲覧専用ウォレット", "global.watched": "視聴済み", "global.watchlist": "ウォッチリスト", + "global.web_access_for_hardware_wallet_disconnected": "ハードウェアウォレット接続のためのWeb アクセスは廃止されました。接続済みのデバイスは引き続き使用できますが、問題が発生した場合は、App またはブラウザ拡張機能をご利用ください。", "global.web_feature_not_available_go_to_app": "この機能はウェブでは利用できません。デスクトップまたはモバイル アプリを使用してください。", "global.website": "ウェブサイト", "global.what_happen": "何が起こるか:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "このバックアップのパスワードを入力してください。", "import_hardware_phrases_warning": "ハードウェアウォレットのリカバリーフレーズをインポートしないでください。 ハードウェアウォレットを接続 ↗ を使用してください", "import_phrase_or_private_key": "フレーズまたは秘密鍵をインポート", + "incorrect_pin": "PINが正しくありません。もう一度お試しください。", "insufficient_fee_append_desc": "最大推定手数料に基づく: {amount} {symbol}", "interact_with_contract": "(に)交流する", "kaspa_official": "Kaspa公式", + "keyless_wallet": "キーレスウォレット", + "keyless_wallet_desc": "キーレスウォレットは、複数の要素に分散して安全に保管されます。GoogleまたはAppleアカウントと4桁のPINで、ウォレットへのアクセスを復元できます。", "learn_how_to_withdraw_crypto_from_exchange": "これらの取引所から暗号資産をOneKeyに出金する方法を学びましょう", "learn_more_about_qr_code_wallet": "QR-basedウォレットの詳細を見る", "lightning_invoice": "ライトニングインボイス", @@ -2044,6 +2060,7 @@ "market.ath_desc": "{token}の史上最高値は{time}に{price}で記録され、現在の価格はその最高値から{percent}下がっています。", "market.atl_desc": "{token}の史上最安値は{time}の{price}で、現在の価格はその最安値から{percent}上昇しています。", "market.cex": "CEXCEX", + "market.change_24h": "24時間変動", "market.chart": "チャート", "market.days_since_launch": "発売からの日数", "market.empty_watchlist_desc": "お気に入りのトークンをウォッチリストに追加してください", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "選択したアカウントでは利用できないネットワーク", "network_show_enabled_only": "有効なネットワークのみを表示", "network_visible_in_all_network_tooltip_title": "「すべてのネットワーク」ビューに表示", + "new_pin_created": "新しいPINが作成されました", + "new_pin_created_desc": "PINはいつでも設定から変更できます。", "nft.already_collected": "このNFTはすでに収集されています。", "nft.attributes": "属性", "nft.collect_failed": "NFTの収集に失敗しました、もう一度お試しください。", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "サイドパネルとして開く", "open_in_mobile_app": "モバイルアプリで開く", "open_ordinals_transfer_tutorial_url_message": "Ordinals資産をどのように転送しますか?", + "open_source_secure_sharding": "オープンソースのセキュアシェーディング", "p2pkh_desc": "\"1\"で始まる。P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "\"bc1q\"で始まります。P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "\"bc1p\"で始まります。P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "もっと必要 {token}?", "perps.get_reward": "報酬を獲得", "perps.offline_moblie": "接続が切断されました。ネットワークを確認して、プルして更新してください。", + "perps.settings_return_to_default_layout": "デフォルトのレイアウトに戻る", "perps.share_position_background": "背景", "perps.share_position_btn_Share_on_x": "Xで共有", "perps.share_position_btn_copy_link": "リンクをコピー", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "株式", "perps.trade_reward": "取引報酬", "pick_your_device": "デバイスを選択", + "pin_attempts_cooldown": "{seconds}後にもう一度お試しください。", + "pin_attempts_remaining": "PINが正しくありません。残り{attemptsRemaining}回の試行が可能です。", "preparing_backup_desc": "少々お待ちください…", "preparing_backup_title": "バックアップを準備中…", "prime.about_cloud_sync": "クラウド同期について", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "ウォレットアドレスを使って受け取る", "receive_from_exchange": "取引所から受け取る", "receive_token_list_footer_text": "トークンが見つからない場合は検索してみてください", + "recovery_phrase_free": "リカバリーフレーズ不要", "recovery_phrase_screenshot_protected_desc": "資産の安全のため、リカバリーフレーズがスクリーンショットに表示されることは決してありません。", "recovery_phrase_screenshot_protected_title": "リカバリーフレーズが保護されています", "referral.accept": "受け入れる", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} 件のアドレス", "referral_code_tutorial_label": "紹介コードの入手方法は?", "referral_promo_title": "OneKey リファラルプログラムに参加する", + "remember_your_pin": "PINを覚えていますか?", + "remember_your_pin_desc": "PINを忘れないようにするための親切なリマインダーです。定期的に確認させていただきます。", "remove_account_desc": "このアカウントは削除されます。", "remove_device": "デバイスを削除", "remove_device_desc": "これにより、このハードウェアウォレットはOneKey Appから切断されます。必要に応じて、Appに再接続することができます。", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "ウォレットを削除する前に、リカバリーフレーズを書き留めておくことを確認してください。そうしないと、ウォレットを復元することができません。", "remove_wallet_double_confirm_message": "リカバリーフレーズを書き留めました", "reset_app_desc": "これにより、OneKey上で作成したすべてのデータが削除されます。適切なバックアップを取ったことを確認した後、アプリをリセットするために\"RESET\"と入力してください。", + "reset_pin": "PINをリセット", "scan.camera_access_denied": "カメラへのアクセスが拒否されました", "scan.enable_camera_permissions": "OneKeyはQRコードをスキャンするためにカメラへのアクセスが必要です。この機能を使用するには、「設定」に移動してカメラの権限を有効にしてください。", "scan.grant_camera_access_in_expand_view": "拡大表示でカメラへのアクセスを許可してください。", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "スキャンしてアドレスを作成", "scanning_text": "スキャニング", "secure_qr_toast_scan_qr_code_on_device_text": "QRコードが表示されたら、「次へ」をクリックしてスキャンしてください。", + "seed_phrase_wallet": "シードフレーズウォレット", "select_connect_app_on_home": "ホーム画面で「Connect App Wallet」を選択", "select_onekey_app": "OneKey App を選択", "select_recovery_phrase_length": "長さを選択", + "select_your_email": "メールアドレスを選択してください", + "select_your_email_desc": "GoogleまたはAppleアカウントでウォレットを追加", "selected_network": "選択されたネットワーク", "selected_network_only_supports_device": "選択されたネットワークは現在、{deviceType}のみをサポートしています", "self_troubleshooting": "セルフトラブルシューティング", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "承認中...", "swap.btn_building": "注文を作成中...", "swap.ch_status_hold": "サポートにお問い合わせください", + "swap.current_token": "現在のトークン", "swap.limit_amount": "{num1} {fromToken}を{num2} {toToken}で売却", "swap.loading_content": "最適な価格を計算中...", "swap.native_token_max_tip": "ネットワーク手数料の支払いのために、少量のトークンを残しておいてください。そうしないと、トランザクションが失敗します。", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "{accelerator} によって提供されるサービス", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "注記", "tx_accelerate.speed_up_with_accelerator_dialog_title": "{accelerator}でスピードアップ", + "ultra_fast_setup": "超高速セットアップ", "update.all_other_apps_closed": "その他のOneKeyアプリとウェブアップグレードツールはすべて閉じられています。", "update.all_other_apps_closed_emoji": "他のすべてのOneKeyアプリとウェブアップグレードツールは閉じられています。🆗", "update.all_updates_complete": "すべての更新が完了しました👏🏻", diff --git a/packages/shared/src/locale/json/ko_KR.json b/packages/shared/src/locale/json/ko_KR.json index ae8e36dc8da8..5a7f448e0664 100644 --- a/packages/shared/src/locale/json/ko_KR.json +++ b/packages/shared/src/locale/json/ko_KR.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "에너지 렌트", "bandwidth_energy.title": "대역폭 & 에너지", "bandwidth_energy.what_is_bandwidth_energy": "트론에서의 대역폭 & 에너지는 무엇인가요?", + "beginner_friendly": "초보자에게 친숙한", "bip44__standard": "BIP44 표준", "bluetooth.disable_in_settings": "USB만 사용하고 싶고 이 알림을 다시 보고 싶지 않다면, OneKey 앱 > 설정 > 환경설정에서 Bluetooth를 끄세요.", "bluetooth.disabled": "블루투스가 비활성화되어 있습니다", @@ -294,6 +295,8 @@ "communication.timeout": "통신 시간 초과", "confirm_exit_dialog_desc": "데이터 마이그레이션 과정을 종료하시겠습니까?", "confirm_exit_dialog_title": "종료 확인", + "confirm_your_pin": "PIN을 확인하세요", + "confirm_your_pin_desc": "이 PIN을 잊어버리면 새 기기에서 지갑을 복구할 수 없습니다.", "connect_device_to_computer_via_usb": "USB를 통해 {deviceLabel}을(를) 컴퓨터에 연결하세요", "connect_with_qr_code": "QR 코드로 연결", "contact_us_instruction": "더 많은 도움이 필요하신가요?", @@ -306,6 +309,7 @@ "content__normal": "보통", "content__slow": "느림", "content__to": "받는 사람", + "continue_with_social_platform": "{platform}(으)로 계속하기", "copy_address_modal_item_create_address_instruction": "주소 생성", "copy_address_modal_title": "계정 주소", "copy_anyway": "어쨌든 복사하기", @@ -314,6 +318,8 @@ "count_assets": "{count} 자산", "count_hidden_assets": "{count} 개의 숨겨진 자산", "count_words": "{length} 단어", + "create_a_pin": "PIN 만들기", + "create_a_pin_desc": "이 문구는 모든 기기에서 지갑을 보호하는 데 사용됩니다. 이 문구는 복구할 수 없습니다.", "create_new_wallet_badge_consists": "복구 구문은 12개의 단어로 구성됩니다", "create_new_wallet_badge_handwritten": "손글씨 백업", "create_new_wallet_badge_keep": "직접 안전하게 보관해야 합니다", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "가장 많이 사용됨", "create_new_wallet_badge_supports": "수백 개의 네트워크를 지원합니다", "create_new_wallet_learn_more": "복구 구문은 지갑 보안의 핵심입니다. 이는 개인 키와 지갑 주소를 생성하고 복원하는 데 사용되는 12개의 일반적인 영어 단어로 구성됩니다. 종이에 직접 적어 안전하게 보관하세요 — 오직 당신만이 자산에 접근할 수 있습니다.", + "create_passcode_desc": "이것을 사용하여 지갑을 잠금 해제합니다.", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "표준 지갑이 생성된 후에는 패스프레이즈를 입력하여 숨겨진 지갑을 만드세요.", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "숨겨진 지갑 생성", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "QR 코드를 표시하기 전에는 패스프레이즈가 필요하지 않습니다. ✅ 버튼을 누르고 코드를 보여주고 앱으로 스캔하여 QR-based 표준 지갑을 생성하세요.", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "잘못된 PIN 코드", "enter_pin.title": "PIN 코드를 입력하세요", "enter_pin_on_app": "앱에서 핀 입력", + "enter_your_pin": "PIN을 입력하세요", + "enter_your_pin_desc": "이 이메일에는 이미 지갑이 생성되어 있습니다. 로그인하려면 PIN을 입력하세요.", "explore.add_bookmark": "북마크에 추가하다", "explore.add_to_whitelist": "화이트리스트에 추가", "explore.addresses_count": "{number} 주소", @@ -1072,6 +1081,7 @@ "for_large_assets": "대형 자산의 경우", "for_reference_only": "참고용으로만 사용하세요", "forgot_password_no_question_mark": "비밀번호를 잊으셨나요", + "forgot_pin": "PIN을 잊으셨나요?", "form.address_error_invalid": "잘못된 주소", "form.address_placeholder": "주소 또는 도메인", "form.amount_placeholder": "금액 입력", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "하드웨어 브리지 다운로드", "global.faqs_firmware_detection": "연결 확인", "global.faqs_forgot_pin": "PIN을 잊으셨나요?", - "global.faqs_reset_wallet": "지갑 재설정", + "global.faqs_reset_device": "기기 초기화", "global.favorites": "즐겨찾기", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "계정 이름 검색", "global.search_address": "주소 검색", "global.search_asset": "자산 검색", + "global.search_everything": "전체 검색", "global.search_no_results_desc": "검색 키워드를 변경해보세요", "global.search_no_results_title": "결과 없음", + "global.search_placeholder_web": "검색", "global.search_tokens": "토큰 검색", "global.secure_install": "보안 설치", "global.security": "보안", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "관찰 전용 지갑", "global.watched": "시청함", "global.watchlist": "관심목록", + "global.web_access_for_hardware_wallet_disconnected": "하드웨어 지갑 연결을 위한 웹 액세스는 중단되었습니다. 이미 연결된 기기는 계속 사용할 수 있습니다. 문제가 발생하면 App 또는 브라우저 확장 프로그램을 사용해 주세요.", "global.web_feature_not_available_go_to_app": "이 기능은 웹에서 사용할 수 없습니다. 데스크톱 또는 모바일 앱을 사용해 주세요。", "global.website": "웹사이트", "global.what_happen": "무슨 일이 일어날까요:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "이 백업의 비밀번호를 입력하세요.", "import_hardware_phrases_warning": "하드웨어 지갑의 복구 구문을 가져오지 마세요. 하드웨어 지갑 연결 ↗을(를) 대신 사용하세요", "import_phrase_or_private_key": "구문 또는 개인 키 가져오기", + "incorrect_pin": "잘못된 PIN입니다. 다시 시도해 주세요.", "insufficient_fee_append_desc": "최대 예상 수수료 기준: {amount} {symbol}", "interact_with_contract": "(~와) 상호 작용하기", "kaspa_official": "Kaspa 공식", + "keyless_wallet": "키리스 월렛", + "keyless_wallet_desc": "당신의 키리스 지갑은 여러 요소에 걸쳐 안전하고 탈중앙화된 방식으로 보관됩니다. Google 또는 Apple 계정과 4자리 PIN을 사용해 지갑에 대한 액세스를 복구할 수 있습니다.", "learn_how_to_withdraw_crypto_from_exchange": "이 거래소들에서 보유한 암호화 자산을 OneKey로 출금하는 방법을 알아보세요", "learn_more_about_qr_code_wallet": "QR-based 지갑에 대해 자세히 알아보기", "lightning_invoice": "라이트닝 인보이스", @@ -2044,6 +2060,7 @@ "market.ath_desc": "{token}의 사상 최고가는 {time}에 {price}였으며, 현재 가격은 그 최고가에서 {percent} 하락했습니다.", "market.atl_desc": "{token} 의 사상 최저가는 {time} 에 {price} 였으며, 현재 가격은 해당 최저가 대비 {percent} 상승했습니다.", "market.cex": "CEX", + "market.change_24h": "24시간 변동", "market.chart": "차트", "market.days_since_launch": "출시 이후 일수", "market.empty_watchlist_desc": "즐겨찾는 자산을 관심목록에 추가해보세요", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "선택한 계정에서는 사용할 수 없는 네트워크", "network_show_enabled_only": "활성화된 네트워크만 표시", "network_visible_in_all_network_tooltip_title": "'모든 네트워크' 보기에서 표시됨", + "new_pin_created": "새 PIN 생성됨", + "new_pin_created_desc": "설정에서 언제든지 PIN을 변경할 수 있습니다.", "nft.already_collected": "이 NFT는 이미 수집되었습니다.", "nft.attributes": "속성", "nft.collect_failed": "NFT 수집에 실패했습니다, 다시 시도해 주세요.", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "측면 패널로 열기", "open_in_mobile_app": "모바일 앱에서 열기", "open_ordinals_transfer_tutorial_url_message": "Ordinals 자산을 어떻게 이전하나요?", + "open_source_secure_sharding": "오픈 소스 보안 셰이딩", "p2pkh_desc": "\"1\"로 시작합니다. P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "\"bc1q\"로 시작합니다. P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "\"bc1p\"로 시작합니다. P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "더 필요해요 {token}?", "perps.get_reward": "보상을 받으세요", "perps.offline_moblie": "연결이 끊겼습니다. 네트워크를 확인한 후 끌어내려 새로고침을 시도하세요.", + "perps.settings_return_to_default_layout": "기본 레이아웃으로 돌아가기", "perps.share_position_background": "배경", "perps.share_position_btn_Share_on_x": "X에서 공유", "perps.share_position_btn_copy_link": "링크 복사", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "주식", "perps.trade_reward": "거래 보상", "pick_your_device": "기기를 선택하세요", + "pin_attempts_cooldown": "{seconds} 후에 다시 시도하세요.", + "pin_attempts_remaining": "잘못된 PIN이 입력되었습니다. ${attemptsRemaining}회 남았습니다.", "preparing_backup_desc": "잠시만요…", "preparing_backup_title": "백업을 준비하는 중…", "prime.about_cloud_sync": "클라우드 동기화에 대하여", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "지갑 주소로 받기", "receive_from_exchange": "거래소에서 받기", "receive_token_list_footer_text": "자산을 찾을 수 없나요? 검색해 보세요", + "recovery_phrase_free": "복구 구문이 필요 없는", "recovery_phrase_screenshot_protected_desc": "자산 보안을 위해, 복구 구문은 스크린샷에 절대 표시되지 않습니다.", "recovery_phrase_screenshot_protected_title": "복구 구문이 보호됨", "referral.accept": "수용하다", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount}개의 주소", "referral_code_tutorial_label": "추천 코드는 어떻게 받나요?", "referral_promo_title": "OneKey 추천 프로그램에 참여하세요", + "remember_your_pin": "PIN 기억나시나요?", + "remember_your_pin_desc": "PIN 번호를 항상 기억해 두시길 친근하게 다시 한 번 알려드립니다. 저희가 가끔씩 확인할 거예요.", "remove_account_desc": "이 계정은 삭제될 것입니다.", "remove_device": "장치 제거", "remove_device_desc": "이 작업은 이 하드웨어 지갑을 OneKey App에서 연결 해제합니다. 언제든지 App에 다시 연결할 수 있습니다.", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "지갑을 제거하기 전에 복구 구문을 적어 두었는지 확인하세요. 그렇지 않으면 지갑을 복구할 수 없습니다.", "remove_wallet_double_confirm_message": "나는 복구 구문을 적어 놓았습니다", "reset_app_desc": "이 작업은 OneKey에서 생성한 모든 데이터를 삭제합니다. 적절한 백업을 갖추었는지 확인한 후 \"RESET\"을 입력하여 앱을 초기화하세요.", + "reset_pin": "PIN 재설정", "scan.camera_access_denied": "카메라 접근이 거부되었습니다", "scan.enable_camera_permissions": "OneKey는 QR 코드를 스캔하기 위해 카메라 접근 권한이 필요합니다. 이 기능을 사용하려면 \"설정\"으로 이동하여 카메라 권한을 활성화해 주세요.", "scan.grant_camera_access_in_expand_view": "확장 뷰에서 카메라 접근 권한을 부여해 주세요.", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "주소를 생성하려면 스캔하세요", "scanning_text": "스캐닝", "secure_qr_toast_scan_qr_code_on_device_text": "QR 코드가 표시되면 '다음'을 클릭하고 스캔하세요.", + "seed_phrase_wallet": "시드 구문 지갑", "select_connect_app_on_home": "홈 화면에서 Connect App Wallet을 선택", "select_onekey_app": "OneKey App 선택", "select_recovery_phrase_length": "길이를 고르세요", + "select_your_email": "이메일을 선택하세요", + "select_your_email_desc": "Google 또는 Apple 계정으로 지갑을 추가하세요", "selected_network": "선택한 네트워크", "selected_network_only_supports_device": "선택한 네트워크는 현재 {deviceType}만 지원합니다.", "self_troubleshooting": "자가 문제 해결", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "승인 중...", "swap.btn_building": "주문 생성 중...", "swap.ch_status_hold": "지원팀에 문의하세요", + "swap.current_token": "현재 토큰", "swap.limit_amount": "{num1} {fromToken}을(를) {num2} {toToken}에 판매", "swap.loading_content": "최적의 가격 계산 중...", "swap.native_token_max_tip": "네트워크 수수료를 지불할 수 있도록 소량의 토큰을 남겨두세요. 그렇지 않으면 거래가 실패할 수 있습니다.", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "{accelerator}에서 제공하는 서비스", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "메모", "tx_accelerate.speed_up_with_accelerator_dialog_title": "{accelerator}와 함께 속도를 높이세요", + "ultra_fast_setup": "초고속 설정", "update.all_other_apps_closed": "모든 기타 OneKey 앱과 웹 업그레이드 도구가 종료되었습니다.", "update.all_other_apps_closed_emoji": "모든 기타 OneKey 앱과 웹 업그레이드 도구가 닫혔습니다. 🆗", "update.all_updates_complete": "모든 업데이트가 완료되었습니다 👏🏻", diff --git a/packages/shared/src/locale/json/pt.json b/packages/shared/src/locale/json/pt.json index 5f0731525b8d..a844fa0184cb 100644 --- a/packages/shared/src/locale/json/pt.json +++ b/packages/shared/src/locale/json/pt.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "Aluguel de Energia", "bandwidth_energy.title": "Largura de banda & Energia", "bandwidth_energy.what_is_bandwidth_energy": "O que é Largura de Banda & Energia no Tron?", + "beginner_friendly": "Amigável para iniciantes", "bip44__standard": "Padrão BIP44", "bluetooth.disable_in_settings": "Se você prefere usar apenas USB e não quer ver este aviso novamente, vá para o aplicativo OneKey > Configurações > Preferências e desative o Bluetooth.", "bluetooth.disabled": "Bluetooth está desativado", @@ -294,6 +295,8 @@ "communication.timeout": "Tempo limite de comunicação", "confirm_exit_dialog_desc": "Tem certeza de que deseja sair do processo de migração de dados?", "confirm_exit_dialog_title": "Confirmar saída", + "confirm_your_pin": "Confirme o seu PIN", + "confirm_your_pin_desc": "Se você esquecer este PIN, não poderá recuperar sua carteira em um novo dispositivo.", "connect_device_to_computer_via_usb": "Conecte {deviceLabel} ao seu computador via USB", "connect_with_qr_code": "Conectar com código QR", "contact_us_instruction": "Precisa de mais ajuda?", @@ -306,6 +309,7 @@ "content__normal": "Normal", "content__slow": "Lento", "content__to": "Para", + "continue_with_social_platform": "Continuar com {platform}", "copy_address_modal_item_create_address_instruction": "Criar endereço", "copy_address_modal_title": "Endereço da conta", "copy_anyway": "Copiar mesmo assim", @@ -314,6 +318,8 @@ "count_assets": "{count} ativos", "count_hidden_assets": "{count} ativos ocultos", "count_words": "{length} palavras", + "create_a_pin": "Criar um PIN", + "create_a_pin_desc": "Isto é usado para proteger a sua carteira em todos os seus dispositivos. Isto não pode ser recuperado.", "create_new_wallet_badge_consists": "A frase de recuperação consiste em 12 palavras", "create_new_wallet_badge_handwritten": "Backup manuscrito", "create_new_wallet_badge_keep": "Você precisa mantê-lo seguro", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "Mais usados", "create_new_wallet_badge_supports": "Suporta centenas de redes", "create_new_wallet_learn_more": "A frase de recuperação é o núcleo da segurança da sua carteira. É composta por 12 palavras comuns em inglês usadas para criar e restaurar sua chave privada e endereço da carteira. Anote-a à mão e guarde-a em segurança — apenas você tem acesso aos seus ativos.", + "create_passcode_desc": "Você usará isso para desbloquear sua carteira.", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "Após a criação da carteira padrão, insira uma passphrase para criar uma carteira oculta.", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "Criar carteira oculta", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "Não é necessário uma passphrase antes de exibir o código QR. Toque no botão ✅, mostre o código e escaneie-o com o aplicativo para criar uma carteira padrão baseada em QR.", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "Código PIN inválido", "enter_pin.title": "Insira o código PIN", "enter_pin_on_app": "Insira o Pin no aplicativo", + "enter_your_pin": "Insira o seu PIN", + "enter_your_pin_desc": "Este e-mail já possui uma carteira criada. Por favor, insira seu PIN para fazer login.", "explore.add_bookmark": "Adicionar marcador", "explore.add_to_whitelist": "adicionar à lista branca", "explore.addresses_count": "{number} endereços", @@ -1072,6 +1081,7 @@ "for_large_assets": "Para ativos grandes", "for_reference_only": "Para referência apenas", "forgot_password_no_question_mark": "Esqueceu a senha", + "forgot_pin": "Esqueceu o PIN?", "form.address_error_invalid": "Endereço inválido", "form.address_placeholder": "Endereço ou domínio", "form.amount_placeholder": "Insira o valor", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "Baixar ponte de comunicação", "global.faqs_firmware_detection": "Teste de conexão", "global.faqs_forgot_pin": "Esqueci o PIN", - "global.faqs_reset_wallet": "Repor carteira", + "global.faqs_reset_device": "Redefinir dispositivo", "global.favorites": "Favoritos", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "Pesquisar nome da conta", "global.search_address": "Procurar endereço", "global.search_asset": "Pesquisar ativo", + "global.search_everything": "Pesquisar tudo", "global.search_no_results_desc": "Tente alterar a palavra-chave da pesquisa", "global.search_no_results_title": "Sem resultados", + "global.search_placeholder_web": "Pesquisar", "global.search_tokens": "Pesquisar token", "global.secure_install": "Instalação segura", "global.security": "Segurança", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "Carteira somente de observação", "global.watched": "Assistido", "global.watchlist": "Lista de observação", + "global.web_access_for_hardware_wallet_disconnected": "O acesso web para conexão de carteira de hardware foi descontinuado. Dispositivos conectados ainda podem ser usados; se encontrar problemas, por favor use o App ou a extensão do navegador.", "global.web_feature_not_available_go_to_app": "Este recurso não está disponível na web. Use o aplicativo para desktop ou móvel.", "global.website": "Website", "global.what_happen": "O que vai acontecer:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "Por favor, insira a senha para este backup.", "import_hardware_phrases_warning": "Não importe a frase de recuperação da sua carteira de hardware. Conectar Carteira de Hardware ↗ em vez disso", "import_phrase_or_private_key": "Importar frase ou chave privada", + "incorrect_pin": "PIN incorreto. Por favor, tente novamente.", "insufficient_fee_append_desc": "com base na taxa máxima estimada: {amount} {symbol}", "interact_with_contract": "Interagir com (Para)", "kaspa_official": "Kaspa Oficial", + "keyless_wallet": "Carteira sem chave", + "keyless_wallet_desc": "A sua carteira sem chave é armazenada de forma segura e descentralizada em vários fatores. Recupere o acesso à sua carteira com a sua conta Google ou Apple e um PIN de 4 dígitos.", "learn_how_to_withdraw_crypto_from_exchange": "Aprenda como retirar criptoativos destas Exchanges para a OneKey", "learn_more_about_qr_code_wallet": "Saiba mais sobre a carteira QR-based", "lightning_invoice": "Fatura Relâmpago", @@ -2044,6 +2060,7 @@ "market.ath_desc": "O recorde histórico de {token} foi em {time}, a {price}, e o preço atual está {percent} abaixo desse recorde.", "market.atl_desc": "A mínima histórica do {token} foi em {time} , a {price} , e o preço atual está {percent} acima dessa mínima.", "market.cex": "CEX", + "market.change_24h": "Variação / 24h", "market.chart": "Gráfico", "market.days_since_launch": "Dias desde o lançamento", "market.empty_watchlist_desc": "Adicione seus tokens favoritos à lista de observação", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "Redes indisponíveis para a conta selecionada", "network_show_enabled_only": "Mostrar apenas redes habilitadas", "network_visible_in_all_network_tooltip_title": "Exibido na visualização 'Todas as redes'", + "new_pin_created": "Novo PIN Criado", + "new_pin_created_desc": "Você pode alterar seu PIN a qualquer momento através das Configurações.", "nft.already_collected": "Este NFT já foi coletado.", "nft.attributes": "Atributos", "nft.collect_failed": "A coleta de NFT falhou, por favor tente novamente.", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "Abrir como painel lateral", "open_in_mobile_app": "Abrir no Aplicativo Móvel", "open_ordinals_transfer_tutorial_url_message": "Como transferir ativos Ordinals?", + "open_source_secure_sharding": "Sombreamento seguro de código aberto", "p2pkh_desc": "Começa com \"1\". P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "Começa com \"bc1q\". P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "Começa com \"bc1p\". P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "Preciso de mais {token}?", "perps.get_reward": "Receba recompensas", "perps.offline_moblie": "Conexão perdida. Verifique sua rede e tente puxar para atualizar.", + "perps.settings_return_to_default_layout": "Retornar ao layout padrão", "perps.share_position_background": "Contexto", "perps.share_position_btn_Share_on_x": "Compartilhar no X", "perps.share_position_btn_copy_link": "Copiar Link", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "Ações", "perps.trade_reward": "Recompensa comercial", "pick_your_device": "Escolha o seu dispositivo", + "pin_attempts_cooldown": "Tente novamente em {seconds}.", + "pin_attempts_remaining": "PIN incorreto inserido. {attemptsRemaining} tentativas restantes.", "preparing_backup_desc": "Só um momento…", "preparing_backup_title": "Preparando backup…", "prime.about_cloud_sync": "Sobre a Sincronização na Nuvem", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "Receber usando o endereço da sua carteira", "receive_from_exchange": "Receber da exchange", "receive_token_list_footer_text": "Não consegue encontrar o token? Tente pesquisar", + "recovery_phrase_free": "Sem frase de recuperação", "recovery_phrase_screenshot_protected_desc": "Para a segurança dos seus ativos, sua frase de recuperação nunca aparecerá em capturas de tela.", "recovery_phrase_screenshot_protected_title": "Frase de recuperação protegida", "referral.accept": "Aceitar", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} endereços", "referral_code_tutorial_label": "Como obter um código de indicação?", "referral_promo_title": "Participe do Programa de Indicação OneKey", + "remember_your_pin": "Lembra-se do seu PIN?", + "remember_your_pin_desc": "Apenas um lembrete amigável para manter o seu PIN fresco na memória. Vamos verificar de tempos em tempos.", "remove_account_desc": "Esta conta será removida.", "remove_device": "Remover dispositivo", "remove_device_desc": "Isso irá desconectar esta carteira de hardware do OneKey App. Você pode reconectá-la ao App sempre que quiser.", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "Certifique-se de ter anotado a frase de recuperação antes de remover a carteira. Caso contrário, você não conseguirá recuperar a carteira.", "remove_wallet_double_confirm_message": "Eu anotei a frase de recuperação", "reset_app_desc": "Isso irá apagar todos os dados que você criou no OneKey. Após garantir que você tem um backup adequado, digite \"RESET\" para redefinir o App", + "reset_pin": "Redefinir PIN", "scan.camera_access_denied": "Acesso à câmera negado", "scan.enable_camera_permissions": "OneKey requer acesso à câmera para escanear códigos QR. Por favor, vá para \"Configurações\" e habilite as permissões da câmera para usar este recurso.", "scan.grant_camera_access_in_expand_view": "Por favor, conceda acesso à câmera na visualização expandida.", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "Escaneie para criar um endereço", "scanning_text": "Digitalização", "secure_qr_toast_scan_qr_code_on_device_text": "Retorne quando o código QR aparecer, clique em 'Próximo', depois escaneie-o.", + "seed_phrase_wallet": "Carteira de frase semente", "select_connect_app_on_home": "Selecione Conectar App Wallet na tela inicial", "select_onekey_app": "Selecionar OneKey App", "select_recovery_phrase_length": "Selecione um comprimento", + "select_your_email": "Selecione o seu e-mail", + "select_your_email_desc": "Adicione uma carteira com sua conta Google ou Apple", "selected_network": "Rede selecionada", "selected_network_only_supports_device": "A rede selecionada atualmente suporta apenas {deviceType}", "self_troubleshooting": "Solução de problemas autônoma", @@ -3283,7 +3313,7 @@ "settings.upload_state_logs": "Carregar registros de estado", "settings.uploading_logs_progress": "Enviando logs… {progress}%", "settings.user_agreement": "Acordo de usuário", - "settings.version_versionnum": "versão {versionNum}", + "settings.version_versionnum": "Versão {versionNum}", "settings.view_address_in_explorer": "Ver endereço no explorador", "settings.view_transaction_in_explorer": "Ver transação no explorador", "settings.whats_new": "O que há de novo", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "Aprovando...", "swap.btn_building": "Construindo pedido...", "swap.ch_status_hold": "Entre em contato com o suporte", + "swap.current_token": "Tokens atuais", "swap.limit_amount": "Vender {num1} {fromToken} por {num2} {toToken}", "swap.loading_content": "Calculando o melhor preço...", "swap.native_token_max_tip": "Por favor, reserve uma pequena quantidade de tokens para pagar as taxas de rede, caso contrário a transação falhará.", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "Serviço fornecido por {accelerator}", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "Observação", "tx_accelerate.speed_up_with_accelerator_dialog_title": "Acelere com {accelerator}", + "ultra_fast_setup": "Configuração ultrarrápida", "update.all_other_apps_closed": "Todos os outros aplicativos OneKey e ferramentas de atualização web estão fechados.", "update.all_other_apps_closed_emoji": "Todos os outros aplicativos OneKey e ferramentas de atualização web estão fechados. 🆗", "update.all_updates_complete": "Todas as atualizações estão completas 👏🏻", diff --git a/packages/shared/src/locale/json/pt_BR.json b/packages/shared/src/locale/json/pt_BR.json index ddc6c2693d0b..8f3daba1b196 100644 --- a/packages/shared/src/locale/json/pt_BR.json +++ b/packages/shared/src/locale/json/pt_BR.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "Aluguel de Energia", "bandwidth_energy.title": "Largura de banda & Energia", "bandwidth_energy.what_is_bandwidth_energy": "O que é Largura de Banda & Energia no Tron?", + "beginner_friendly": "Amigável para iniciantes", "bip44__standard": "Padrão BIP44", "bluetooth.disable_in_settings": "Se você prefere usar apenas USB e não quer ver este aviso novamente, vá para o aplicativo OneKey > Configurações > Preferências e desative o Bluetooth.", "bluetooth.disabled": "Bluetooth está desativado", @@ -294,6 +295,8 @@ "communication.timeout": "Tempo limite de comunicação", "confirm_exit_dialog_desc": "Você tem certeza de que deseja sair do processo de migração de dados?", "confirm_exit_dialog_title": "Confirmar saída", + "confirm_your_pin": "Confirme seu PIN", + "confirm_your_pin_desc": "Se você esquecer este PIN, não poderá recuperar sua carteira em um novo dispositivo.", "connect_device_to_computer_via_usb": "Conecte {deviceLabel} ao seu computador via USB", "connect_with_qr_code": "Conectar com código QR", "contact_us_instruction": "Precisa de mais ajuda?", @@ -306,6 +309,7 @@ "content__normal": "Normal", "content__slow": "Lento", "content__to": "Para", + "continue_with_social_platform": "Continuar com {platform}", "copy_address_modal_item_create_address_instruction": "Criar endereço", "copy_address_modal_title": "Endereço da conta", "copy_anyway": "Copiar mesmo assim", @@ -314,6 +318,8 @@ "count_assets": "{count} ativos", "count_hidden_assets": "{count} ativos ocultos", "count_words": "{length} palavras", + "create_a_pin": "Criar um PIN", + "create_a_pin_desc": "Isso é usado para proteger sua carteira em todos os seus dispositivos. Isso não pode ser recuperado.", "create_new_wallet_badge_consists": "A frase de recuperação consiste em 12 palavras", "create_new_wallet_badge_handwritten": "Backup manuscrito", "create_new_wallet_badge_keep": "Você precisa mantê-lo seguro", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "Mais usados", "create_new_wallet_badge_supports": "Suporta centenas de redes", "create_new_wallet_learn_more": "A frase de recuperação é o núcleo da segurança da sua carteira. Ela é composta por 12 palavras comuns em inglês usadas para criar e restaurar sua chave privada e endereço da carteira. Anote-a à mão e guarde-a com segurança — somente você tem acesso aos seus ativos.", + "create_passcode_desc": "Você usará isso para desbloquear sua carteira.", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "Após a criação da carteira padrão, insira uma passphrase para criar uma carteira oculta.", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "Criar carteira oculta", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "Não é necessário uma passphrase antes de exibir o código QR. Toque no botão ✅, mostre o código e escaneie-o com o aplicativo para criar uma carteira padrão baseada em QR.", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "Código PIN inválido", "enter_pin.title": "Insira o código PIN", "enter_pin_on_app": "Insira o PIN no aplicativo", + "enter_your_pin": "Digite seu PIN", + "enter_your_pin_desc": "Este e-mail já possui uma carteira criada. Por favor, insira seu PIN para fazer login.", "explore.add_bookmark": "Adicionar marcador", "explore.add_to_whitelist": "adicionar à lista branca", "explore.addresses_count": "{number} endereços", @@ -1072,6 +1081,7 @@ "for_large_assets": "Para ativos grandes", "for_reference_only": "Para referência apenas", "forgot_password_no_question_mark": "Esqueceu a senha", + "forgot_pin": "Esqueceu o PIN?", "form.address_error_invalid": "Endereço inválido", "form.address_placeholder": "Endereço ou domínio", "form.amount_placeholder": "Insira o valor", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "Baixar ponte de comunicação", "global.faqs_firmware_detection": "Teste de conexão", "global.faqs_forgot_pin": "Esqueci o PIN", - "global.faqs_reset_wallet": "Redefinir carteira", + "global.faqs_reset_device": "Redefinir dispositivo", "global.favorites": "Favoritos", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "Pesquisar nome da conta", "global.search_address": "Buscar endereço", "global.search_asset": "Pesquisar ativo", + "global.search_everything": "Pesquisar tudo", "global.search_no_results_desc": "Tente alterar a palavra-chave da pesquisa", "global.search_no_results_title": "Sem resultados", + "global.search_placeholder_web": "Pesquisar", "global.search_tokens": "Buscar token", "global.secure_install": "Instalação segura", "global.security": "Segurança", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "Carteira somente de observação", "global.watched": "Assistido", "global.watchlist": "Lista de observação", + "global.web_access_for_hardware_wallet_disconnected": "O acesso web para conexão de carteira de hardware foi descontinuado. Dispositivos conectados ainda podem ser usados; se você encontrar problemas, por favor use o App ou a extensão do navegador.", "global.web_feature_not_available_go_to_app": "Este recurso não está disponível na web. Por favor, use o aplicativo de desktop ou móvel.", "global.website": "Site", "global.what_happen": "O que acontecerá:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "Por favor, insira a senha para este backup.", "import_hardware_phrases_warning": "Não importe a frase de recuperação da sua carteira de hardware. Conectar Carteira de Hardware ↗ em vez disso", "import_phrase_or_private_key": "Importar frase ou chave privada", + "incorrect_pin": "PIN incorreto. Tente novamente.", "insufficient_fee_append_desc": "com base na taxa máxima estimada: {amount} {symbol}", "interact_with_contract": "Interagir com (Para)", "kaspa_official": "Kaspa Oficial", + "keyless_wallet": "Carteira sem chave", + "keyless_wallet_desc": "Sua carteira sem chave é armazenada de forma segura e descentralizada em vários fatores. Recupere o acesso à sua carteira com sua conta do Google ou Apple e um PIN de 4 dígitos.", "learn_how_to_withdraw_crypto_from_exchange": "Aprenda como sacar criptoativos dessas Exchanges para a OneKey", "learn_more_about_qr_code_wallet": "Saiba mais sobre a carteira QR-based", "lightning_invoice": "Fatura Relâmpago", @@ -2044,6 +2060,7 @@ "market.ath_desc": "O recorde histórico de {token} foi em {time}, a {price}, e o preço atual está {percent} abaixo desse recorde.", "market.atl_desc": "A mínima histórica do {token} foi em {time} , a {price} , e o preço atual está {percent} acima dessa mínima.", "market.cex": "CEX", + "market.change_24h": "Variação / 24h", "market.chart": "Gráfico", "market.days_since_launch": "Dias desde o lançamento", "market.empty_watchlist_desc": "Adicione seus tokens favoritos à lista de observação", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "Redes indisponíveis para a conta selecionada", "network_show_enabled_only": "Mostrar apenas redes habilitadas", "network_visible_in_all_network_tooltip_title": "Exibido na visualização 'Todas as redes'", + "new_pin_created": "Novo PIN Criado", + "new_pin_created_desc": "Você pode alterar seu PIN a qualquer momento através das Configurações.", "nft.already_collected": "Este NFT já foi coletado.", "nft.attributes": "Atributos", "nft.collect_failed": "A coleta de NFT falhou, por favor tente novamente.", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "Abrir como painel lateral", "open_in_mobile_app": "Abrir no Aplicativo Móvel", "open_ordinals_transfer_tutorial_url_message": "Como transferir ativos Ordinals?", + "open_source_secure_sharding": "Sombreamento seguro de código aberto", "p2pkh_desc": "Começa com \"1\". P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "Começa com \"bc1q\". P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "Começa com \"bc1p\". P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "Preciso de mais {token}?", "perps.get_reward": "Receba recompensas", "perps.offline_moblie": "Conexão perdida. Verifique sua rede e tente puxar para atualizar.", + "perps.settings_return_to_default_layout": "Retornar ao layout padrão", "perps.share_position_background": "Histórico", "perps.share_position_btn_Share_on_x": "Compartilhar no X", "perps.share_position_btn_copy_link": "Copiar Link", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "Ações", "perps.trade_reward": "Recompensa comercial", "pick_your_device": "Escolha seu dispositivo", + "pin_attempts_cooldown": "Tente novamente em {seconds}.", + "pin_attempts_remaining": "PIN incorreto inserido. ${attemptsRemaining} tentativas restantes.", "preparing_backup_desc": "Só um momento…", "preparing_backup_title": "Preparando backup…", "prime.about_cloud_sync": "Sobre a Sincronização na Nuvem", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "Receber usando o endereço da sua carteira", "receive_from_exchange": "Receber da exchange", "receive_token_list_footer_text": "Não consegue encontrar o token? Tente pesquisar", + "recovery_phrase_free": "Sem frase de recuperação", "recovery_phrase_screenshot_protected_desc": "Para a segurança dos seus ativos, sua frase de recuperação nunca aparecerá em capturas de tela.", "recovery_phrase_screenshot_protected_title": "Frase de recuperação protegida", "referral.accept": "Aceitar", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} endereços", "referral_code_tutorial_label": "Como obter um código de indicação?", "referral_promo_title": "Participe do Programa de Indicação OneKey", + "remember_your_pin": "Lembra do seu PIN?", + "remember_your_pin_desc": "Apenas um lembrete amigável para manter seu PIN fresco na memória. Vamos verificar de tempos em tempos.", "remove_account_desc": "Esta conta será removida.", "remove_device": "Remover dispositivo", "remove_device_desc": "Isso irá desconectar esta carteira de hardware do OneKey App. Você pode reconectá-la ao App sempre que quiser.", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "Certifique-se de ter anotado a frase de recuperação antes de remover a carteira. Caso contrário, você não conseguirá recuperar a carteira.", "remove_wallet_double_confirm_message": "Eu anotei a frase de recuperação", "reset_app_desc": "Isso irá deletar todos os dados que você criou no OneKey. Após garantir que você tem um backup adequado, digite \"RESET\" para redefinir o App", + "reset_pin": "Redefinir PIN", "scan.camera_access_denied": "Acesso à câmera negado", "scan.enable_camera_permissions": "OneKey requer acesso à câmera para escanear códigos QR. Por favor, vá para \"Configurações\" e habilite as permissões da câmera para usar este recurso.", "scan.grant_camera_access_in_expand_view": "Por favor, conceda acesso à câmera na visualização expandida.", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "Escaneie para criar um endereço", "scanning_text": "Digitalização", "secure_qr_toast_scan_qr_code_on_device_text": "Retorne quando o código QR aparecer, clique em 'Próximo', depois escaneie-o.", + "seed_phrase_wallet": "Carteira de frase semente", "select_connect_app_on_home": "Selecione Conectar App Wallet na tela inicial", "select_onekey_app": "Selecionar OneKey App", "select_recovery_phrase_length": "Selecione um comprimento", + "select_your_email": "Selecione seu e-mail", + "select_your_email_desc": "Adicione uma carteira com sua conta do Google ou Apple", "selected_network": "Rede selecionada", "selected_network_only_supports_device": "A rede selecionada atualmente suporta apenas {deviceType}", "self_troubleshooting": "Autodiagnóstico", @@ -3283,7 +3313,7 @@ "settings.upload_state_logs": "Enviar logs de estado", "settings.uploading_logs_progress": "Enviando logs… {progress}%", "settings.user_agreement": "Acordo de usuário", - "settings.version_versionnum": "versão {versionNum}", + "settings.version_versionnum": "Versão {versionNum}", "settings.view_address_in_explorer": "Visualizar endereço no explorador", "settings.view_transaction_in_explorer": "Visualizar transação no explorador", "settings.whats_new": "O que há de novo", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "Aprovando...", "swap.btn_building": "Criando pedido...", "swap.ch_status_hold": "Entre em contato com o suporte", + "swap.current_token": "Tokens atuais", "swap.limit_amount": "Vender {num1} {fromToken} por {num2} {toToken}", "swap.loading_content": "Calculando o melhor preço...", "swap.native_token_max_tip": "Por favor, reserve uma pequena quantidade de tokens para pagar as taxas de rede, caso contrário a transação falhará.", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "Serviço fornecido por {accelerator}", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "Observação", "tx_accelerate.speed_up_with_accelerator_dialog_title": "Acelere com {accelerator}", + "ultra_fast_setup": "Configuração ultrarrápida", "update.all_other_apps_closed": "Todos os outros aplicativos OneKey e ferramentas de atualização web estão fechados.", "update.all_other_apps_closed_emoji": "Todos os outros aplicativos OneKey e ferramentas de atualização web estão fechados. 🆗", "update.all_updates_complete": "Todas as atualizações estão completas 👏🏻", diff --git a/packages/shared/src/locale/json/ru.json b/packages/shared/src/locale/json/ru.json index 2bd51d93fbfd..b564c5ade1cd 100644 --- a/packages/shared/src/locale/json/ru.json +++ b/packages/shared/src/locale/json/ru.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "Аренда Энергии", "bandwidth_energy.title": "Пропускная способность и энергия", "bandwidth_energy.what_is_bandwidth_energy": "Что такое пропускная способность и энергия в Tron?", + "beginner_friendly": "Удобно для новичков", "bip44__standard": "Стандарт BIP44", "bluetooth.disable_in_settings": "Если вы предпочитаете использовать только USB и не хотите больше видеть это уведомление, откройте приложение OneKey > Настройки > Предпочтения и отключите Bluetooth.", "bluetooth.disabled": "Bluetooth отключен", @@ -294,6 +295,8 @@ "communication.timeout": "Тайм-аут связи", "confirm_exit_dialog_desc": "Вы уверены, что хотите выйти из процесса миграции данных?", "confirm_exit_dialog_title": "Подтвердите выход", + "confirm_your_pin": "Подтвердите ваш PIN-код", + "confirm_your_pin_desc": "Если вы забудете этот PIN-код, вы не сможете восстановить свой кошелек на новом устройстве.", "connect_device_to_computer_via_usb": "Подключите {deviceLabel} к вашему компьютеру через USB", "connect_with_qr_code": "Подключиться по QR‑коду", "contact_us_instruction": "Нужна дополнительная помощь?", @@ -306,6 +309,7 @@ "content__normal": "Нормальный", "content__slow": "Медленно", "content__to": "К", + "continue_with_social_platform": "Продолжить с {platform}", "copy_address_modal_item_create_address_instruction": "Создать адрес", "copy_address_modal_title": "Адрес аккаунта", "copy_anyway": "Скопировать в любом случае", @@ -314,6 +318,8 @@ "count_assets": "{count} активов", "count_hidden_assets": "{count} скрытых активов", "count_words": "{length} слов", + "create_a_pin": "Создать PIN-код", + "create_a_pin_desc": "Это используется для защиты вашего кошелька на всех ваших устройствах. Это невозможно восстановить.", "create_new_wallet_badge_consists": "Фраза для восстановления состоит из 12 слов", "create_new_wallet_badge_handwritten": "Резервная копия, записанная вручную", "create_new_wallet_badge_keep": "Нужно самому позаботиться о безопасности", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "Чаще всего используемые", "create_new_wallet_badge_supports": "Поддерживает сотни сетей", "create_new_wallet_learn_more": "Фраза восстановления — основа безопасности вашего кошелька. Она состоит из 12 распространённых английских слов, с помощью которых создаются и восстанавливаются ваш приватный ключ и адрес кошелька. Запишите её от руки и храните в надёжном месте — только у вас есть доступ к вашим активам.", + "create_passcode_desc": "Вы будете использовать это для разблокировки вашего кошелька.", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "После создания стандартного кошелька введите passphrase для создания скрытого кошелька.", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "Создать скрытый кошелек", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "Перед отображением QR-кода не требуется passphrase. Нажмите кнопку ✅, покажите код и отсканируйте его с помощью приложения, чтобы создать стандартный кошелек на основе QR.", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "Неверный код PIN", "enter_pin.title": "Введите PIN-код", "enter_pin_on_app": "Введите Пин-код в приложении", + "enter_your_pin": "Введите ваш PIN-код", + "enter_your_pin_desc": "Для этого адреса электронной почты уже создан кошелек. Пожалуйста, введите свой PIN-код для входа.", "explore.add_bookmark": "Добавить закладку", "explore.add_to_whitelist": "добавить в белый список", "explore.addresses_count": "{number} адресов", @@ -1072,6 +1081,7 @@ "for_large_assets": "Для крупных активов", "for_reference_only": "Только для справки", "forgot_password_no_question_mark": "Забыли пароль", + "forgot_pin": "Забыли PIN-код?", "form.address_error_invalid": "Недействительный адрес", "form.address_placeholder": "Адрес или домен", "form.amount_placeholder": "Введите сумму", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "Скачать мост связи", "global.faqs_firmware_detection": "Проверка связи", "global.faqs_forgot_pin": "Забыли PIN?", - "global.faqs_reset_wallet": "Сбросить кошелек", + "global.faqs_reset_device": "Сбросить устройство", "global.favorites": "Избранное", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "Поиск по имени учетной записи", "global.search_address": "Поиск адреса", "global.search_asset": "Поиск активов", + "global.search_everything": "Искать везде", "global.search_no_results_desc": "Попробуйте изменить ключевое слово для поиска", "global.search_no_results_title": "Нет результатов", + "global.search_placeholder_web": "Поиск", "global.search_tokens": "Поиск токена", "global.secure_install": "Безопасная установка", "global.security": "Безопасность", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "Кошелек только для просмотра", "global.watched": "Просмотрено", "global.watchlist": "Список наблюдения", + "global.web_access_for_hardware_wallet_disconnected": "Веб-доступ для подключения аппаратного кошелька был прекращен. Подключенные устройства по-прежнему можно использовать; если у вас возникнут проблемы, пожалуйста, используйте App или расширение для браузера.", "global.web_feature_not_available_go_to_app": "Эта функция недоступна в интернете. Пожалуйста, используйте настольное или мобильное приложение.", "global.website": "Веб-сайт", "global.what_happen": "Что произойдет:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "Пожалуйста, введите пароль для этой резервной копии.", "import_hardware_phrases_warning": "Не импортируйте секретную фразу восстановления аппаратного кошелька. Подключить аппаратный кошелёк ↗ вместо этого", "import_phrase_or_private_key": "Импортировать сид-фразу или приватный ключ", + "incorrect_pin": "Неверный PIN-код. Повторите попытку.", "insufficient_fee_append_desc": "на основе макс. оценочной комиссии: {amount} {symbol}", "interact_with_contract": "Взаимодействовать с (К)", "kaspa_official": "Kaspa Официальный", + "keyless_wallet": "Кошелек без ключей", + "keyless_wallet_desc": "Ваш кошелек без ключей хранится безопасно и децентрализованно на нескольких факторах. Восстановите доступ к кошельку с помощью учетной записи Google или Apple и 4-значного PIN-кода.", "learn_how_to_withdraw_crypto_from_exchange": "Узнайте, как вывести криптоактивы с этих бирж на OneKey", "learn_more_about_qr_code_wallet": "Узнайте больше о QR-based кошельке", "lightning_invoice": "Счет по молнии", @@ -2044,6 +2060,7 @@ "market.ath_desc": "Исторический максимум {token} был достигнут {time} по цене {price}, и текущая цена ниже этого максимума на {percent}.", "market.atl_desc": "Абсолютный минимум {token} был зафиксирован {time} на уровне {price} , а текущая цена выросла на {percent} от этого минимума.", "market.cex": "CEX", + "market.change_24h": "Изменение / 24ч", "market.chart": "Диаграмма", "market.days_since_launch": "Дней с момента запуска", "market.empty_watchlist_desc": "Добавьте ваши любимые токены в список для отслеживания", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "Недоступные сети для выбранного аккаунта", "network_show_enabled_only": "Показывать только включенные сети", "network_visible_in_all_network_tooltip_title": "Показано в виде \"Все сети\"", + "new_pin_created": "Новый PIN-код создан", + "new_pin_created_desc": "Вы можете изменить свой PIN-код в любое время через Настройки.", "nft.already_collected": "Этот NFT уже был собран.", "nft.attributes": "Атрибуты", "nft.collect_failed": "Сбой сбора NFT, пожалуйста, попробуйте еще раз.", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "Открыть как боковую панель", "open_in_mobile_app": "Открыть в мобильном приложении", "open_ordinals_transfer_tutorial_url_message": "Как перенести активы Ordinals?", + "open_source_secure_sharding": "Открытый исходный код безопасного затенения", "p2pkh_desc": "Начинается с \"1\". P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "Начинается с \"bc1q\". P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "Начинается с \"bc1p\". P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "Нужно больше {token}?", "perps.get_reward": "Получите награды", "perps.offline_moblie": "Соединение потеряно. Проверьте подключение к сети и попробуйте обновить, потянув вниз.", + "perps.settings_return_to_default_layout": "Вернуться к макету по умолчанию", "perps.share_position_background": "Фон", "perps.share_position_btn_Share_on_x": "Поделиться в X", "perps.share_position_btn_copy_link": "Копировать ссылку", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "Акции", "perps.trade_reward": "Торговое вознаграждение", "pick_your_device": "Выберите своё устройство", + "pin_attempts_cooldown": "Повторите попытку через {seconds}.", + "pin_attempts_remaining": "Введен неверный PIN-код. Осталось попыток: ${attemptsRemaining}.", "preparing_backup_desc": "Минутку…", "preparing_backup_title": "Подготовка резервной копии…", "prime.about_cloud_sync": "О облачной синхронизации", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "Получайте с помощью адреса вашего кошелька", "receive_from_exchange": "Получить с биржи", "receive_token_list_footer_text": "Не можете найти токен? Попробуйте поискать", + "recovery_phrase_free": "Без фразы восстановления", "recovery_phrase_screenshot_protected_desc": "Для безопасности ваших активов ваша фраза восстановления никогда не будет отображаться на скриншотах.", "recovery_phrase_screenshot_protected_title": "Секретная фраза защищена", "referral.accept": "Принимать", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} адресов", "referral_code_tutorial_label": "Как получить реферальный код?", "referral_promo_title": "Присоединяйтесь к реферальной программе OneKey", + "remember_your_pin": "Помните свой PIN-код?", + "remember_your_pin_desc": "Просто дружеское напоминание, чтобы ваш PIN-код оставался свежим в памяти. Мы будем проверять это время от времени.", "remove_account_desc": "Этот аккаунт будет удален.", "remove_device": "Удалить устройство", "remove_device_desc": "Это отключит этот аппаратный кошелек от приложения OneKey. Вы можете снова подключить его к приложению в любое время.", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "Убедитесь, что вы записали фразу восстановления перед удалением кошелька. В противном случае, вы не сможете восстановить кошелек.", "remove_wallet_double_confirm_message": "Я записал фразу для восстановления", "reset_app_desc": "Это удалит все данные, которые вы создали в OneKey. После того как вы убедитесь, что у вас есть надлежащая резервная копия, введите \"RESET\", чтобы сбросить приложение", + "reset_pin": "Сбросить ПИН-код", "scan.camera_access_denied": "Доступ к камере запрещен", "scan.enable_camera_permissions": "OneKey требует доступа к камере для сканирования QR-кодов. Пожалуйста, перейдите в \"Настройки\" и включите разрешения для камеры, чтобы использовать эту функцию.", "scan.grant_camera_access_in_expand_view": "Пожалуйста, предоставьте доступ к камере в расширенном виде.", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "Сканируйте, чтобы создать адрес", "scanning_text": "Сканирование", "secure_qr_toast_scan_qr_code_on_device_text": "Вернитесь, когда появится QR-код, нажмите 'Далее', затем отсканируйте его.", + "seed_phrase_wallet": "Кошелек с сид-фразой", "select_connect_app_on_home": "Выберите Connect App Wallet на главном экране", "select_onekey_app": "Выберите App OneKey", "select_recovery_phrase_length": "Выберите длину", + "select_your_email": "Выберите вашу электронную почту", + "select_your_email_desc": "Добавьте кошелек с помощью аккаунта Google или Apple", "selected_network": "Выбранная сеть", "selected_network_only_supports_device": "Выбранная сеть в настоящее время поддерживает только {deviceType}", "self_troubleshooting": "Самостоятельное устранение неполадок", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "Одобрение...", "swap.btn_building": "Создание заказа...", "swap.ch_status_hold": "Связаться со службой поддержки", + "swap.current_token": "Текущие токены", "swap.limit_amount": "Продать {num1} {fromToken} за {num2} {toToken}", "swap.loading_content": "Вычисление лучшей цены...", "swap.native_token_max_tip": "Пожалуйста, оставьте небольшое количество токенов для оплаты сетевых комиссий, иначе транзакция не пройдет.", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "Услуга предоставляется {accelerator}", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "Примечание", "tx_accelerate.speed_up_with_accelerator_dialog_title": "Ускорьтесь с {accelerator}", + "ultra_fast_setup": "Сверхбыстрая настройка", "update.all_other_apps_closed": "Все остальные приложения OneKey и инструменты для веб-обновления закрыты.", "update.all_other_apps_closed_emoji": "Все остальные приложения OneKey и инструменты для веб-обновления закрыты. 🆗", "update.all_updates_complete": "Все обновления завершены 👏🏻", diff --git a/packages/shared/src/locale/json/th_TH.json b/packages/shared/src/locale/json/th_TH.json index bdcfcda84ade..0e5742fe9dbc 100644 --- a/packages/shared/src/locale/json/th_TH.json +++ b/packages/shared/src/locale/json/th_TH.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "เช่าพลังงาน", "bandwidth_energy.title": "แบนด์วิดธ์และพลังงาน", "bandwidth_energy.what_is_bandwidth_energy": "คืออะไรคือแบนด์วิดท์และพลังงานบน Tron?", + "beginner_friendly": "เหมาะสำหรับผู้เริ่มต้น", "bip44__standard": "มาตรฐาน BIP44", "bluetooth.disable_in_settings": "หากคุณต้องการใช้เฉพาะ USB และไม่ต้องการเห็นข้อความแจ้งเตือนนี้อีก ให้ไปที่แอป OneKey > การตั้งค่า > การตั้งค่ากำหนดเอง แล้วปิด Bluetooth", "bluetooth.disabled": "Bluetooth ถูกปิดใช้งาน", @@ -294,6 +295,8 @@ "communication.timeout": "หมดเวลาการสื่อสาร", "confirm_exit_dialog_desc": "คุณแน่ใจหรือไม่ว่าต้องการออกจากกระบวนการโยกย้ายข้อมูล?", "confirm_exit_dialog_title": "ยืนยันการออก", + "confirm_your_pin": "ยืนยันรหัส PIN ของคุณ", + "confirm_your_pin_desc": "หากคุณลืม PIN นี้ คุณจะไม่สามารถกู้คืนกระเป๋าเงินของคุณบนอุปกรณ์ใหม่ได้", "connect_device_to_computer_via_usb": "เชื่อมต่อ {deviceLabel} กับคอมพิวเตอร์ของคุณผ่าน USB", "connect_with_qr_code": "เชื่อมต่อด้วยคิวอาร์โค้ด", "contact_us_instruction": "ต้องการความช่วยเหลือเพิ่มเติมหรือไม่?", @@ -306,6 +309,7 @@ "content__normal": "ปกติ", "content__slow": "ช้า", "content__to": "ถึง", + "continue_with_social_platform": "ดำเนินการต่อด้วย {platform}", "copy_address_modal_item_create_address_instruction": "สร้างที่อยู่", "copy_address_modal_title": "ที่อยู่บัญชี", "copy_anyway": "คัดลอกอยู่ดี", @@ -314,6 +318,8 @@ "count_assets": "{count} สินทรัพย์", "count_hidden_assets": "{count} สินทรัพย์ที่ซ่อนอยู่", "count_words": "{length} คำ", + "create_a_pin": "สร้างรหัส PIN", + "create_a_pin_desc": "นี่จะถูกใช้เพื่อรักษาความปลอดภัยกระเป๋าเงินของคุณบนทุกอุปกรณ์ ไม่สามารถกู้คืนได้.", "create_new_wallet_badge_consists": "วลีการกู้คืนประกอบด้วย 12 คำ", "create_new_wallet_badge_handwritten": "สำรองข้อมูลแบบเขียนด้วยลายมือ", "create_new_wallet_badge_keep": "ต้องเก็บรักษาให้ปลอดภัยด้วยตัวคุณเอง", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "ใช้งานบ่อยที่สุด", "create_new_wallet_badge_supports": "รองรับเครือข่ายนับร้อย", "create_new_wallet_learn_more": "วลีการกู้คืนคือแกนกลางของความปลอดภัยกระเป๋าเงินของคุณ ประกอบด้วยคำภาษาอังกฤษทั่วไป 12 คำที่ใช้สร้างและกู้คืนกุญแจส่วนตัวและที่อยู่กระเป๋าเงินของคุณ จดด้วยลายมือและเก็บรักษาไว้ให้ปลอดภัย — มีเพียงคุณเท่านั้นที่เข้าถึงทรัพย์สินของคุณได้", + "create_passcode_desc": "คุณจะใช้สิ่งนี้เพื่อปลดล็อกกระเป๋าเงินของคุณ", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "หลังจากสร้างกระเป๋าเงินมาตรฐานแล้ว ใส่ Passphrase เพื่อสร้างกระเป๋าเงินที่ซ่อนอยู่", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "สร้างกระเป๋าเงินที่ซ่อนอยู่", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "ไม่จำเป็นต้องใช้ passphrase ก่อนที่จะแสดง QR code แตะที่ปุ่ม ✅ แสดงรหัสและสแกนด้วยแอปเพื่อสร้างกระเป๋าเงินมาตรฐานที่ใช้ QR-based", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "รหัส PIN ไม่ถูกต้อง", "enter_pin.title": "ใส่รหัส PIN", "enter_pin_on_app": "ใส่รหัส PIN ในแอป", + "enter_your_pin": "ใส่รหัส PIN ของคุณ", + "enter_your_pin_desc": "อีเมลนี้มีการสร้างวอลเล็ตไว้แล้ว กรุณากรอกรหัส PIN ของคุณเพื่อเข้าสู่ระบบ", "explore.add_bookmark": "เพิ่มบุ๊กมาร์ก", "explore.add_to_whitelist": "เพิ่มไปยังรายการที่ได้รับอนุญาต", "explore.addresses_count": "{number} ที่อยู่", @@ -1072,6 +1081,7 @@ "for_large_assets": "สำหรับสินทรัพย์ขนาดใหญ่", "for_reference_only": "เพื่ออ้างอิงเท่านั้น", "forgot_password_no_question_mark": "ลืมรหัสผ่าน", + "forgot_pin": "ลืมรหัส PIN ใช่ไหม?", "form.address_error_invalid": "ที่อยู่ไม่ถูกต้อง", "form.address_placeholder": "ที่อยู่หรือโดเมน", "form.amount_placeholder": "กรอกจำนวน", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "ดาวน์โหลดบริดจ์การสื่อสารฮาร์ดแวร์", "global.faqs_firmware_detection": "ตรวจสอบการเชื่อมต่อ", "global.faqs_forgot_pin": "ลืม PIN", - "global.faqs_reset_wallet": "รีเซ็ตวอลเล็ต", + "global.faqs_reset_device": "รีเซ็ตอุปกรณ์", "global.favorites": "รายการโปรด", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "ค้นหาชื่อบัญชี", "global.search_address": "ค้นหาที่อยู่", "global.search_asset": "ค้นหาสินทรัพย์", + "global.search_everything": "ค้นหาทุกอย่าง", "global.search_no_results_desc": "ลองเปลี่ยนคำค้นหา", "global.search_no_results_title": "ไม่พบผลลัพธ์", + "global.search_placeholder_web": "ค้นหา", "global.search_tokens": "ค้นหาโทเค็น", "global.secure_install": "ติดตั้งอย่างปลอดภัย", "global.security": "ความปลอดภัย", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "กระเป๋าเงินแบบดูเท่านั้น", "global.watched": "ดูแล้ว", "global.watchlist": "รายการที่ติดตาม", + "global.web_access_for_hardware_wallet_disconnected": "การเชื่อมต่อกระเป๋าเงินฮาร์ดแวร์ผ่านเว็บถูกยกเลิกแล้ว อุปกรณ์ที่เชื่อมต่อไว้ยังสามารถใช้งานได้ หากพบปัญหา โปรดใช้ App หรือส่วนขยายเบราว์เซอร์แทน", "global.web_feature_not_available_go_to_app": "ฟีเจอร์นี้ไม่พร้อมใช้งานบนเว็บ โปรดใช้แอปเดสก์ท็อปหรือมือถือ", "global.website": "เว็บไซต์", "global.what_happen": "สิ่งที่จะเกิดขึ้น:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "โปรดป้อนรหัสผ่านสำหรับข้อมูลสำรองนี้", "import_hardware_phrases_warning": "อย่านำเข้าวลีการกู้คืนของฮาร์ดแวร์วอลเล็ตของคุณ เชื่อมต่อฮาร์ดแวร์วอลเล็ต ↗ แทน", "import_phrase_or_private_key": "นำเข้าวลีหรือกุญแจส่วนตัว", + "incorrect_pin": "รหัส PIN ไม่ถูกต้อง โปรดลองอีกครั้ง", "insufficient_fee_append_desc": "ตามค่าธรรมเนียมสูงสุดที่ประมาณ: {amount} {symbol}", "interact_with_contract": "ติดต่อกับ (ถึง)", "kaspa_official": "Kaspa อย่างเป็นทางการ", + "keyless_wallet": "กระเป๋าเงินไร้กุญแจ", + "keyless_wallet_desc": "กระเป๋าสตางค์แบบไม่ใช้กุญแจของคุณถูกเก็บไว้อย่างปลอดภัยแบบกระจายตัวบนปัจจัยหลายส่วน คุณสามารถกู้คืนการเข้าถึงกระเป๋าสตางค์ของคุณได้ด้วยบัญชี Google หรือ Apple และรหัส PIN 4 หลัก", "learn_how_to_withdraw_crypto_from_exchange": "เรียนรู้วิธีถอนทรัพย์สินคริปโตจากแพลตฟอร์มแลกเปลี่ยนเหล่านี้ไปยัง OneKey", "learn_more_about_qr_code_wallet": "เรียนรู้เพิ่มเติมเกี่ยวกับกระเป๋าเงินแบบ QR-based", "lightning_invoice": "ใบแจ้งหนี้แสงฟ้า", @@ -2044,6 +2060,7 @@ "market.ath_desc": "ราคาสูงสุดตลอดกาลของ {token} อยู่เมื่อวันที่ {time} ที่ {price} และราคาปัจจุบันลดลง {percent} จากจุดสูงสุดนั้น", "market.atl_desc": "ราคาต่ำสุดตลอดกาลของ {token} อยู่ที่ {time} ที่ {price} และราคาปัจจุบันเพิ่มขึ้น {percent} จากราคาต่ำสุดนั้น", "market.cex": "CEX", + "market.change_24h": "การเปลี่ยนแปลง / 24 ชม.", "market.chart": "แผนภูมิ", "market.days_since_launch": "วันนับตั้งแต่เปิดตัว", "market.empty_watchlist_desc": "เพิ่มโทเค็นที่คุณชื่นชอบลงในรายการที่ต้องสังเกต", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "เครือข่ายไม่พร้อมใช้งานสำหรับบัญชีที่เลือก", "network_show_enabled_only": "แสดงเฉพาะเครือข่ายที่เปิดใช้งาน", "network_visible_in_all_network_tooltip_title": "แสดงในมุมมอง 'ทุกเครือข่าย'", + "new_pin_created": "สร้างรหัส PIN ใหม่แล้ว", + "new_pin_created_desc": "คุณสามารถเปลี่ยนรหัส PIN ของคุณได้ทุกเมื่อผ่านการตั้งค่า", "nft.already_collected": "NFT นี้ถูกเก็บไว้แล้ว", "nft.attributes": "คุณสมบัติ", "nft.collect_failed": "การรวบรวม NFT ล้มเหลว โปรดลองอีกครั้ง", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "เปิดเป็นแผงด้านข้าง", "open_in_mobile_app": "เปิดในแอปมือถือ", "open_ordinals_transfer_tutorial_url_message": "วิธีการโอนทรัพย์สิน Ordinals คืออย่างไร?", + "open_source_secure_sharding": "การไล่เฉดสีอย่างปลอดภัยแบบโอเพนซอร์ส", "p2pkh_desc": "เริ่มต้นด้วย \"1\". P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "เริ่มต้นด้วย \"bc1q\". P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "เริ่มต้นด้วย \"bc1p\". P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "ต้องการเพิ่มเติม {token}-", "perps.get_reward": "รับรางวัล", "perps.offline_moblie": "การเชื่อมต่อขาดหาย โปรดตรวจสอบเครือข่ายของคุณแล้วลองดึงหน้าลงเพื่อรีเฟรชอีกครั้ง", + "perps.settings_return_to_default_layout": "กลับไปใช้เลย์เอาต์เริ่มต้น", "perps.share_position_background": "พื้นหลัง", "perps.share_position_btn_Share_on_x": "แชร์บน X", "perps.share_position_btn_copy_link": "คัดลอกลิงก์", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "หุ้น", "perps.trade_reward": "ผลตอบแทนการค้า", "pick_your_device": "เลือกอุปกรณ์ของคุณ", + "pin_attempts_cooldown": "ลองอีกครั้งใน {seconds}.", + "pin_attempts_remaining": "ป้อนรหัส PIN ไม่ถูกต้อง เหลืออีก ${attemptsRemaining} ครั้ง", "preparing_backup_desc": "สักครู่…", "preparing_backup_title": "กำลังเตรียมสำรองข้อมูล…", "prime.about_cloud_sync": "เกี่ยวกับคลาวด์ซิงค์", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "รับโดยใช้ที่อยู่กระเป๋าเงินของคุณ", "receive_from_exchange": "รับจากแพลตฟอร์มซื้อขาย", "receive_token_list_footer_text": "ไม่พบโทเค็นใช่ไหม ลองค้นหาดู", + "recovery_phrase_free": "ไม่มีวลีสำหรับกู้คืน", "recovery_phrase_screenshot_protected_desc": "เพื่อความปลอดภัยของสินทรัพย์ของคุณ วลีการกู้คืนของคุณจะ ไม่ปรากฏในภาพหน้าจออย่างเด็ดขาด.", "recovery_phrase_screenshot_protected_title": "วลีการกู้คืนได้รับการปกป้อง", "referral.accept": "ยอมรับ", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} ที่อยู่", "referral_code_tutorial_label": "จะขอรับรหัสแนะนำได้อย่างไร?", "referral_promo_title": "เข้าร่วมโปรแกรมแนะนำเพื่อน OneKey", + "remember_your_pin": "จำ PIN ของคุณได้ไหม?", + "remember_your_pin_desc": "แค่เตือนกันอย่างเป็นกันเองให้คุณจำรหัส PIN ให้แม่นนะ เราจะถามย้ำเป็นระยะ ๆ", "remove_account_desc": "บัญชีนี้จะถูกลบออก", "remove_device": "นำอุปกรณ์ออก", "remove_device_desc": "การดำเนินการนี้จะตัดการเชื่อมต่อกระเป๋าสตางค์ฮาร์ดแวร์นี้ออกจาก OneKey App คุณสามารถเชื่อมต่อใหม่กับ App ได้ทุกเมื่อที่ต้องการ", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "ตรวจสอบให้แน่ใจว่าคุณได้เขียน recovery phrase ไว้ก่อนที่จะลบกระเป๋าเงิน มิฉะนั้นคุณจะไม่สามารถกู้คืนกระเป๋าเงินได้", "remove_wallet_double_confirm_message": "ฉันได้เขียน recovery phrase ลงไปแล้ว", "reset_app_desc": "การดำเนินการนี้จะลบข้อมูลทั้งหมดที่คุณสร้างบน OneKey หลังจากที่คุณแน่ใจว่ามีการสำรองข้อมูลอย่างเหมาะสมแล้ว ให้ป้อน \"RESET\" เพื่อรีเซ็ตแอป", + "reset_pin": "รีเซ็ต PIN", "scan.camera_access_denied": "การเข้าถึงกล้องถูกปฏิเสธ", "scan.enable_camera_permissions": "OneKey ต้องการการเข้าถึงกล้องเพื่อสแกน QR codes กรุณาไปที่ \"การตั้งค่า\" และเปิดใช้งานสิทธิ์การใช้กล้องเพื่อใช้คุณลักษณะนี้", "scan.grant_camera_access_in_expand_view": "โปรดอนุญาตให้เข้าถึงกล้องในมุมมองขยาย", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "สแกนเพื่อสร้างที่อยู่", "scanning_text": "การสแกน", "secure_qr_toast_scan_qr_code_on_device_text": "เมื่อรหัส QR ปรากฏขึ้น คลิก 'ถัดไป' แล้วสแกนรหัสดังกล่าว", + "seed_phrase_wallet": "กระเป๋าสตางค์วลีช่วยจำ", "select_connect_app_on_home": "เลือก Connect App Wallet บนหน้าจอหลัก", "select_onekey_app": "เลือกแอป OneKey", "select_recovery_phrase_length": "เลือกความยาว", + "select_your_email": "เลือกอีเมลของคุณ", + "select_your_email_desc": "เพิ่มกระเป๋าเงินด้วยบัญชี Google หรือ Apple ของคุณ", "selected_network": "เครือข่ายที่เลือก", "selected_network_only_supports_device": "เครือข่ายที่เลือกในปัจจุบันรองรับเฉพาะ {deviceType} เท่านั้น", "self_troubleshooting": "การแก้ปัญหาด้วยตนเอง", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "กำลังอนุมัติ...", "swap.btn_building": "กำลังสร้างคำสั่งซื้อ...", "swap.ch_status_hold": "ติดต่อฝ่ายสนับสนุน", + "swap.current_token": "โทเค็นปัจจุบัน", "swap.limit_amount": "ขาย {num1} {fromToken} เป็น {num2} {toToken}", "swap.loading_content": "กำลังคำนวณราคาที่ดีที่สุด...", "swap.native_token_max_tip": "กรุณาเผื่อโทเค็นจำนวนเล็กน้อยไว้สำหรับชำระค่าธรรมเนียมเครือข่าย มิฉะนั้นธุรกรรมจะล้มเหลว", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "บริการที่จัดหาโดย {accelerator}", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "บันทึก", "tx_accelerate.speed_up_with_accelerator_dialog_title": "เร่งความเร็วด้วย {accelerator}", + "ultra_fast_setup": "ติดตั้งได้อย่างรวดเร็วเป็นพิเศษ", "update.all_other_apps_closed": "แอป OneKey อื่น ๆ และเครื่องมืออัปเกรดเว็บทั้งหมดถูกปิดไปแล้ว", "update.all_other_apps_closed_emoji": "แอป OneKey อื่น ๆ และเครื่องมืออัปเกรดเว็บทั้งหมดถูกปิดแล้ว. 🆗", "update.all_updates_complete": "การอัปเดตทั้งหมดเสร็จสิ้น 👏🏻", diff --git a/packages/shared/src/locale/json/uk_UA.json b/packages/shared/src/locale/json/uk_UA.json index 163687a36669..4c667d0fe533 100644 --- a/packages/shared/src/locale/json/uk_UA.json +++ b/packages/shared/src/locale/json/uk_UA.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "Оренда енергії", "bandwidth_energy.title": "Пропускна здатність та енергія", "bandwidth_energy.what_is_bandwidth_energy": "Що таке пропускна здатність та енергія на Tron?", + "beginner_friendly": "Зручний для початківців", "bip44__standard": "Стандарт BIP44", "bluetooth.disable_in_settings": "Якщо ви віддаєте перевагу використанню лише USB і не хочете більше бачити це повідомлення, перейдіть до додатку OneKey > Налаштування > Параметри та вимкніть Bluetooth.", "bluetooth.disabled": "Bluetooth вимкнено", @@ -294,6 +295,8 @@ "communication.timeout": "Час очікування зв'язку вичерпано", "confirm_exit_dialog_desc": "Ви впевнені, що хочете вийти з процесу міграції даних?", "confirm_exit_dialog_title": "Підтвердіть вихід", + "confirm_your_pin": "Підтвердьте свій PIN-код", + "confirm_your_pin_desc": "Якщо ви забудете цей PIN-код, ви не зможете відновити свій гаманець на новому пристрої.", "connect_device_to_computer_via_usb": "Підключіть {deviceLabel} до комп'ютера через USB", "connect_with_qr_code": "Підключитися за допомогою QR-коду", "contact_us_instruction": "Потрібна додаткова допомога?", @@ -306,6 +309,7 @@ "content__normal": "Нормальний", "content__slow": "Повільно", "content__to": "До", + "continue_with_social_platform": "Продовжити з {platform}", "copy_address_modal_item_create_address_instruction": "Створіть адресу", "copy_address_modal_title": "Адреса рахунку", "copy_anyway": "Копіювати все одно", @@ -314,6 +318,8 @@ "count_assets": "{count} активів", "count_hidden_assets": "{count} приховані активи", "count_words": "{length} слів", + "create_a_pin": "Створити PIN-код", + "create_a_pin_desc": "Це використовується для захисту вашого гаманця на всіх ваших пристроях. Це неможливо відновити.", "create_new_wallet_badge_consists": "Фраза відновлення складається з 12 слів", "create_new_wallet_badge_handwritten": "Рукописна резервна копія", "create_new_wallet_badge_keep": "Потрібно зберігати це в безпеці самостійно", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "Найчастіше використовувані", "create_new_wallet_badge_supports": "Підтримує сотні мереж", "create_new_wallet_learn_more": "Фраза відновлення — це основа безпеки вашого гаманця. Вона складається з 12 звичайних англійських слів, які використовуються для створення та відновлення вашого приватного ключа та адреси гаманця. Запишіть її від руки та зберігайте в безпечному місці — тільки ви маєте доступ до своїх активів.", + "create_passcode_desc": "Ви використаєте це для розблокування свого гаманця.", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "Після створення стандартного гаманця, введіть passphrase для створення прихованого гаманця.", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "Створити прихований гаманець", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "Не потрібно вводити Passphrase перед відображенням QR-коду. Натисніть кнопку ✅, покажіть код і відскануйте його за допомогою додатка, щоб створити стандартний гаманець на основі QR.", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "Неправильний PIN-код", "enter_pin.title": "Введіть PIN-код", "enter_pin_on_app": "Введіть ПІН-код у додатку", + "enter_your_pin": "Введіть свій PIN-код", + "enter_your_pin_desc": "Для цієї електронної адреси вже створено гаманець. Будь ласка, введіть свій PIN-код для входу.", "explore.add_bookmark": "Додати закладку", "explore.add_to_whitelist": "додати до білого списку", "explore.addresses_count": "{number} адреси", @@ -1072,6 +1081,7 @@ "for_large_assets": "Для великих активів", "for_reference_only": "Тільки для довідки", "forgot_password_no_question_mark": "Забули пароль", + "forgot_pin": "Забули PIN-код?", "form.address_error_invalid": "Неправильна адреса", "form.address_placeholder": "Адреса або домен", "form.amount_placeholder": "Введіть суму", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "Завантажити міст зв'язку", "global.faqs_firmware_detection": "Перевірка зв'язку", "global.faqs_forgot_pin": "Забули PIN?", - "global.faqs_reset_wallet": "Скинути гаманець", + "global.faqs_reset_device": "Скинути пристрій", "global.favorites": "Улюблені", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "Пошук назви облікового запису", "global.search_address": "Шукати адресу", "global.search_asset": "Пошук активів", + "global.search_everything": "Шукати все", "global.search_no_results_desc": "Спробуйте змінити ключове слово пошуку", "global.search_no_results_title": "Немає результатів", + "global.search_placeholder_web": "Пошук", "global.search_tokens": "Пошуковий токен", "global.secure_install": "Безпечна установка", "global.security": "Безпека", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "Гаманець лише для перегляду", "global.watched": "Переглянуто", "global.watchlist": "Список спостереження", + "global.web_access_for_hardware_wallet_disconnected": "Веб-доступ для підключення апаратного гаманця припинено. Підключені пристрої все ще можна використовувати; якщо виникнуть проблеми, будь ласка, скористайтеся App або розширенням для браузера.", "global.web_feature_not_available_go_to_app": "Ця функція недоступна в інternetі. Будь ласка, використовуйте настільний або мобільний додаток.", "global.website": "Вебсайт", "global.what_happen": "Що станеться:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "Будь ласка, введіть пароль для цієї резервної копії.", "import_hardware_phrases_warning": "Не імпортуйте фразу відновлення вашого апаратного гаманця. Підключити апаратний гаманець ↗ натомість", "import_phrase_or_private_key": "Імпортувати фразу або приватний ключ", + "incorrect_pin": "Неправильний PIN-код. Будь ласка, спробуйте ще раз.", "insufficient_fee_append_desc": "на основі макс. оціненої комісії: {amount} {symbol}", "interact_with_contract": "Взаємодіяти з (До)", "kaspa_official": "Kaspa Офіційний", + "keyless_wallet": "Гаманець без ключа", + "keyless_wallet_desc": "Ваш гаманець без ключів зберігається безпечно та децентралізовано на кількох факторах. Відновіть доступ до свого гаманця за допомогою облікового запису Google або Apple та 4-значного PIN-коду.", "learn_how_to_withdraw_crypto_from_exchange": "Дізнайтеся, як виводити криптоактиви з цих бірж на OneKey", "learn_more_about_qr_code_wallet": "Дізнайтеся більше про QR-based гаманець", "lightning_invoice": "Рахунок-фактура за блискавку", @@ -2044,6 +2060,7 @@ "market.ath_desc": "Історичний максимум {token} був досягнутий {time} за ціною {price}, а поточна ціна знизилася на {percent} від цього максимуму.", "market.atl_desc": "Найнижчий за весь час {token} був {time} за {price} , а поточна ціна зросла на {percent} від цього мінімуму.", "market.cex": "CEX", + "market.change_24h": "Зміна / 24г", "market.chart": "Діаграма", "market.days_since_launch": "Днів з моменту запуску", "market.empty_watchlist_desc": "Додайте свої улюблені токени до списку спостереження", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "Недоступні мережі для вибраного облікового запису", "network_show_enabled_only": "Показати лише увімкнені мережі", "network_visible_in_all_network_tooltip_title": "Показано у вигляді \"Всі мережі\"", + "new_pin_created": "Новий PIN створено", + "new_pin_created_desc": "Ви можете змінити свій PIN-код у будь-який час через Налаштування.", "nft.already_collected": "Цей NFT вже було зібрано.", "nft.attributes": "Атрибути", "nft.collect_failed": "Не вдалося зібрати NFT, будь ласка, спробуйте ще раз.", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "Відкрити як бічну панель", "open_in_mobile_app": "Відкрити в мобільному додатку", "open_ordinals_transfer_tutorial_url_message": "Як передати активи Ordinals?", + "open_source_secure_sharding": "Відкрите безпечне затінення", "p2pkh_desc": "Починається з \"1\". P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "Починається з \"bc1q\". P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "Починається з \"bc1p\". P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "Потрібно більше {token}?", "perps.get_reward": "Отримайте винагороди", "perps.offline_moblie": "З'єднання втрачено. Будь ласка, перевірте мережу та спробуйте оновити потягнувши вниз.", + "perps.settings_return_to_default_layout": "Повернутися до макета за замовчуванням", "perps.share_position_background": "Фон", "perps.share_position_btn_Share_on_x": "Поділитися в X", "perps.share_position_btn_copy_link": "Копіювати посилання", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "Акції", "perps.trade_reward": "Торгова винагорода", "pick_your_device": "Виберіть свій пристрій", + "pin_attempts_cooldown": "Спробуйте ще раз через {seconds}.", + "pin_attempts_remaining": "Введено неправильний PIN-код. Залишилося ${attemptsRemaining} спроб.", "preparing_backup_desc": "Зачекайте…", "preparing_backup_title": "Підготовка резервної копії…", "prime.about_cloud_sync": "Про хмарну синхронізацію", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "Отримати, використовуючи адресу вашого гаманця", "receive_from_exchange": "Отримати з біржі", "receive_token_list_footer_text": "Не можете знайти маркер? Спробуйте пошукати", + "recovery_phrase_free": "Без фрази відновлення", "recovery_phrase_screenshot_protected_desc": "Для безпеки ваших активів ваша фраза відновлення ніколи не з'явиться на знімках екрана.", "recovery_phrase_screenshot_protected_title": "Фраза відновлення захищена", "referral.accept": "Прийняти", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} адрес", "referral_code_tutorial_label": "Як отримати реферальний код?", "referral_promo_title": "Приєднуйтесь до реферальної програми OneKey", + "remember_your_pin": "Пам'ятаєте свій PIN-код?", + "remember_your_pin_desc": "Просто дружнє нагадування тримати свій PIN-код свіжим у пам'яті. Ми перевірятимемо час від часу.", "remove_account_desc": "Цей обліковий запис буде видалено.", "remove_device": "Видалити пристрій", "remove_device_desc": "Це відключить цей апаратний гаманець від OneKey App. Ви можете підключити його до App у будь-який час.", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "Переконайтеся, що ви записали фразу відновлення перед видаленням гаманця. В іншому випадку, ви не зможете відновити гаманець.", "remove_wallet_double_confirm_message": "Я записав фразу відновлення", "reset_app_desc": "Це видалить всі дані, які ви створили на OneKey. Після перевірки наявності належної резервної копії, введіть \"RESET\", щоб скинути додаток", + "reset_pin": "Скинути PIN-код", "scan.camera_access_denied": "Доступ до камери заборонено", "scan.enable_camera_permissions": "OneKey вимагає доступу до камери для сканування QR-кодів. Будь ласка, перейдіть до \"Налаштувань\" та включіть дозволи на камеру, щоб використовувати цю функцію.", "scan.grant_camera_access_in_expand_view": "Будь ласка, надайте доступ до камери у розгорнутому вигляді.", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "Скануйте, щоб створити адресу", "scanning_text": "Сканування", "secure_qr_toast_scan_qr_code_on_device_text": "Поверніться, коли з'явиться QR-код, натисніть 'Далі’, а потім відскануйте його.", + "seed_phrase_wallet": "Гаманець з seed-фразою", "select_connect_app_on_home": "Виберіть Connect App Wallet на головному екрані", "select_onekey_app": "Виберіть OneKey App", "select_recovery_phrase_length": "Виберіть довжину", + "select_your_email": "Виберіть свою електронну адресу", + "select_your_email_desc": "Додайте гаманець за допомогою облікового запису Google або Apple", "selected_network": "Вибрана мережа", "selected_network_only_supports_device": "Обрана мережа наразі підтримує лише {deviceType}", "self_troubleshooting": "Самостійне усунення несправностей", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "Підтвердження...", "swap.btn_building": "Створення замовлення...", "swap.ch_status_hold": "Зверніться до служби підтримки", + "swap.current_token": "Поточні токени", "swap.limit_amount": "Продати {num1} {fromToken} за {num2} {toToken}", "swap.loading_content": "Розрахунок найкращої ціни...", "swap.native_token_max_tip": "Будь ласка, зарезервуйте невелику кількість токенів для оплати мережевих комісій, інакше транзакція не вдасться.", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "Послуга надається {accelerator}", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "Примітка", "tx_accelerate.speed_up_with_accelerator_dialog_title": "Прискорюйтесь з {accelerator}", + "ultra_fast_setup": "Ультрашвидке налаштування", "update.all_other_apps_closed": "Всі інші додатки OneKey та інструменти веб-оновлення закриті.", "update.all_other_apps_closed_emoji": "Всі інші додатки OneKey та інструменти веб-оновлення закриті. 🆗", "update.all_updates_complete": "Всі оновлення завершено 👏🏻", diff --git a/packages/shared/src/locale/json/vi.json b/packages/shared/src/locale/json/vi.json index 1663e8e7a56e..93edd36a70fe 100644 --- a/packages/shared/src/locale/json/vi.json +++ b/packages/shared/src/locale/json/vi.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "Thuê Năng Lượng", "bandwidth_energy.title": "Băng thông & Năng lượng", "bandwidth_energy.what_is_bandwidth_energy": "Bandwidth & Energy trên Tron là gì?", + "beginner_friendly": "Thân thiện với người mới bắt đầu", "bip44__standard": "Tiêu chuẩn BIP44", "bluetooth.disable_in_settings": "Nếu bạn chỉ muốn sử dụng USB và không muốn thấy thông báo này nữa, hãy vào ứng dụng OneKey > Cài đặt > Tùy chọn và tắt Bluetooth.", "bluetooth.disabled": "Bluetooth đã bị tắt", @@ -294,6 +295,8 @@ "communication.timeout": "Hết thời gian chờ giao tiếp", "confirm_exit_dialog_desc": "Bạn có chắc chắn muốn thoát khỏi quá trình di chuyển dữ liệu không?", "confirm_exit_dialog_title": "Xác nhận thoát", + "confirm_your_pin": "Xác nhận mã PIN của bạn", + "confirm_your_pin_desc": "Nếu bạn quên mã PIN này, bạn sẽ không thể khôi phục ví của mình trên thiết bị mới.", "connect_device_to_computer_via_usb": "Kết nối {deviceLabel} với máy tính của bạn qua USB", "connect_with_qr_code": "Kết nối bằng mã QR", "contact_us_instruction": "Cần thêm trợ giúp?", @@ -306,6 +309,7 @@ "content__normal": "Bình thường", "content__slow": "Chậm", "content__to": "Đến", + "continue_with_social_platform": "Tiếp tục với {platform}", "copy_address_modal_item_create_address_instruction": "Tạo địa chỉ", "copy_address_modal_title": "Địa chỉ tài khoản", "copy_anyway": "Sao chép dù sao", @@ -314,6 +318,8 @@ "count_assets": "{count} tài sản", "count_hidden_assets": "{count} tài sản bị ẩn", "count_words": "{length} từ", + "create_a_pin": "Tạo mã PIN", + "create_a_pin_desc": "Điều này được sử dụng để bảo mật ví của bạn trên tất cả các thiết bị. Không thể khôi phục được.", "create_new_wallet_badge_consists": "Cụm từ khôi phục gồm 12 từ", "create_new_wallet_badge_handwritten": "Bản sao lưu viết tay", "create_new_wallet_badge_keep": "Bạn cần tự giữ nó an toàn", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "Được dùng nhiều nhất", "create_new_wallet_badge_supports": "Hỗ trợ hàng trăm mạng lưới", "create_new_wallet_learn_more": "Cụm từ khôi phục là cốt lõi của bảo mật ví của bạn. Nó gồm 12 từ tiếng Anh phổ biến dùng để tạo và khôi phục khóa riêng và địa chỉ ví. Hãy viết nó ra bằng tay và cất giữ an toàn — chỉ bạn mới có quyền truy cập vào tài sản của mình.", + "create_passcode_desc": "Bạn sẽ sử dụng cái này để mở khóa ví của mình.", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "Sau khi ví tiêu chuẩn được tạo, hãy nhập một passphrase để tạo một ví ẩn.", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "Tạo ví ẩn", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "Không cần passphrase trước khi hiển thị mã QR. Chạm vào nút ✅, hiển thị mã và quét nó với ứng dụng để tạo một ví chuẩn dựa trên QR.", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "Mã PIN không hợp lệ", "enter_pin.title": "Nhập mã PIN", "enter_pin_on_app": "Nhập Pin trên ứng dụng", + "enter_your_pin": "Nhập mã PIN của bạn", + "enter_your_pin_desc": "Email này đã có ví được tạo. Vui lòng nhập mã PIN của bạn để đăng nhập.", "explore.add_bookmark": "Thêm dấu trang", "explore.add_to_whitelist": "thêm vào danh sách trắng", "explore.addresses_count": "{number} địa chỉ", @@ -1072,6 +1081,7 @@ "for_large_assets": "Dành cho tài sản lớn", "for_reference_only": "Chỉ để tham khảo", "forgot_password_no_question_mark": "Quên mật khẩu", + "forgot_pin": "Quên mã PIN?", "form.address_error_invalid": "Địa chỉ không hợp lệ", "form.address_placeholder": "Địa chỉ hoặc tên miền", "form.amount_placeholder": "Nhập số tiền", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "Tải xuống cầu nối phần cứng", "global.faqs_firmware_detection": "Kiểm tra kết nối", "global.faqs_forgot_pin": "Quên mã PIN", - "global.faqs_reset_wallet": "Đặt lại ví", + "global.faqs_reset_device": "Đặt lại thiết bị", "global.favorites": "Yêu thích", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "Tìm kiếm tên tài khoản", "global.search_address": "Tìm kiếm địa chỉ", "global.search_asset": "Tìm kiếm tài sản", + "global.search_everything": "Tìm kiếm tất cả", "global.search_no_results_desc": "Thử thay đổi từ khóa tìm kiếm", "global.search_no_results_title": "Không có kết quả", + "global.search_placeholder_web": "Tìm kiếm", "global.search_tokens": "Tìm kiếm token", "global.secure_install": "Cài đặt an toàn", "global.security": "Bảo vệ", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "Ví chỉ xem", "global.watched": "Đã xem", "global.watchlist": "Danh sách theo dõi", + "global.web_access_for_hardware_wallet_disconnected": "Quyền truy cập web để kết nối ví cứng đã bị ngừng. Các thiết bị đã kết nối vẫn có thể được sử dụng; nếu bạn gặp vấn đề, vui lòng sử dụng App hoặc tiện ích mở rộng trình duyệt.", "global.web_feature_not_available_go_to_app": "Tính năng này không có trên web. Vui lòng sử dụng ứng dụng dành cho máy tính để bàn hoặc thiết bị di động.", "global.website": "Trang web", "global.what_happen": "Điều gì sẽ xảy ra:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "Vui lòng nhập mật khẩu cho bản sao lưu này.", "import_hardware_phrases_warning": "Đừng nhập cụm từ khôi phục của ví phần cứng. Kết nối ví phần cứng ↗ thay vào đó", "import_phrase_or_private_key": "Nhập cụm từ hoặc khóa riêng tư", + "incorrect_pin": "Mã PIN không đúng. Vui lòng thử lại.", "insufficient_fee_append_desc": "dựa trên phí ước tính tối đa: {amount} {symbol}", "interact_with_contract": "Tương tác với (Đến)", "kaspa_official": "Kaspa Chính Thức", + "keyless_wallet": "Ví không cần khóa", + "keyless_wallet_desc": "Ví không cần khóa của bạn được lưu trữ an toàn và phi tập trung trên nhiều yếu tố. Khôi phục quyền truy cập vào ví của bạn bằng tài khoản Google hoặc Apple và mã PIN 4 chữ số.", "learn_how_to_withdraw_crypto_from_exchange": "Tìm hiểu cách rút tài sản tiền mã hóa từ các sàn giao dịch này về OneKey", "learn_more_about_qr_code_wallet": "Tìm hiểu thêm về ví dựa trên QR-based", "lightning_invoice": "Hóa đơn Lightning", @@ -2044,6 +2060,7 @@ "market.ath_desc": "Mức cao nhất mọi thời đại của {token} là vào ngày {time}, ở mức {price}, và giá hiện tại đã giảm {percent} so với mức cao đó.", "market.atl_desc": "Mức thấp nhất mọi thời đại của {token} là vào {time} , tại {price} và giá hiện tại tăng {percent} so với mức thấp đó.", "market.cex": "CEX", + "market.change_24h": "Thay đổi / 24h", "market.chart": "Biểu đồ", "market.days_since_launch": "Ngày kể từ khi ra mắt", "market.empty_watchlist_desc": "Thêm các token yêu thích của bạn vào danh sách theo dõi", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "Mạng không khả dụng cho tài khoản đã chọn", "network_show_enabled_only": "Chỉ hiển thị các mạng đã bật", "network_visible_in_all_network_tooltip_title": "Hiển thị trong chế độ xem 'Tất cả mạng'", + "new_pin_created": "Đã Tạo Mã PIN Mới", + "new_pin_created_desc": "Bạn có thể thay đổi mã PIN của mình bất cứ lúc nào thông qua Cài đặt.", "nft.already_collected": "NFT này đã được thu thập.", "nft.attributes": "Thuộc tính", "nft.collect_failed": "Thu thập NFT thất bại, vui lòng thử lại.", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "Mở dưới dạng bảng điều khiển bên", "open_in_mobile_app": "Mở trong Ứng dụng Di động", "open_ordinals_transfer_tutorial_url_message": "Làm thế nào để chuyển giao tài sản Ordinals?", + "open_source_secure_sharding": "Che phủ bảo mật mã nguồn mở", "p2pkh_desc": "Bắt đầu bằng \"1\". P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "Bắt đầu bằng \"bc1q\". P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "Bắt đầu với \"bc1p\". P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "Cần thêm nữa {token}?", "perps.get_reward": "Nhận phần thưởng", "perps.offline_moblie": "Mất kết nối. Vui lòng kiểm tra mạng và thử kéo xuống để làm mới.", + "perps.settings_return_to_default_layout": "Quay lại bố cục mặc định", "perps.share_position_background": "Nền tảng", "perps.share_position_btn_Share_on_x": "Chia sẻ trên X", "perps.share_position_btn_copy_link": "Sao chép liên kết", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "Cổ phiếu", "perps.trade_reward": "Phần thưởng thương mại", "pick_your_device": "Chọn thiết bị của bạn", + "pin_attempts_cooldown": "Thử lại sau {seconds}.", + "pin_attempts_remaining": "Đã nhập sai mã PIN. Còn lại ${attemptsRemaining} lần thử.", "preparing_backup_desc": "Chờ một chút…", "preparing_backup_title": "Đang chuẩn bị sao lưu…", "prime.about_cloud_sync": "Về Cloud Sync", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "Nhận bằng địa chỉ ví của bạn", "receive_from_exchange": "Nhận từ sàn giao dịch", "receive_token_list_footer_text": "Không tìm thấy token? Hãy thử tìm kiếm", + "recovery_phrase_free": "Không cần cụm từ khôi phục", "recovery_phrase_screenshot_protected_desc": "Để bảo vệ tài sản của bạn, cụm từ khôi phục của bạn sẽ không bao giờ xuất hiện trong ảnh chụp màn hình.", "recovery_phrase_screenshot_protected_title": "Cụm từ khôi phục đã được bảo vệ", "referral.accept": "Chấp nhận", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} địa chỉ", "referral_code_tutorial_label": "Làm thế nào để nhận mã giới thiệu?", "referral_promo_title": "Tham gia Chương trình Giới thiệu OneKey", + "remember_your_pin": "Bạn có nhớ mã PIN không?", + "remember_your_pin_desc": "Chỉ là lời nhắc nhở thân thiện để bạn luôn nhớ mã PIN của mình. Chúng tôi sẽ kiểm tra thỉnh thoảng.", "remove_account_desc": "Tài khoản này sẽ bị xóa.", "remove_device": "Gỡ bỏ thiết bị", "remove_device_desc": "Điều này sẽ ngắt kết nối ví cứng này khỏi ứng dụng OneKey. Bạn có thể kết nối lại với ứng dụng bất cứ khi nào bạn muốn.", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "Hãy chắc chắn bạn đã ghi lại cụm từ khôi phục trước khi gỡ ví đi. Nếu không, bạn sẽ không thể khôi phục lại ví.", "remove_wallet_double_confirm_message": "Tôi đã ghi lại cụm từ khôi phục", "reset_app_desc": "Điều này sẽ xóa tất cả dữ liệu bạn đã tạo trên OneKey. Sau khi đảm bảo rằng bạn đã sao lưu đúng cách, nhập \"RESET\" để đặt lại Ứng dụng", + "reset_pin": "Đặt lại mã PIN", "scan.camera_access_denied": "Quyền truy cập camera bị từ chối", "scan.enable_camera_permissions": "OneKey yêu cầu quyền truy cập vào camera để quét mã QR. Vui lòng vào “Cài đặt” và bật quyền truy cập camera để sử dụng tính năng này.", "scan.grant_camera_access_in_expand_view": "Vui lòng cấp quyền truy cập camera trong chế độ xem mở rộng.", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "Quét để tạo địa chỉ", "scanning_text": "Quét", "secure_qr_toast_scan_qr_code_on_device_text": "Khi mã QR xuất hiện, nhấp vào 'Tiếp theo', sau đó quét nó.", + "seed_phrase_wallet": "Ví cụm từ khôi phục", "select_connect_app_on_home": "Chọn Connect App Wallet trên màn hình chính", "select_onekey_app": "Chọn App OneKey", "select_recovery_phrase_length": "Chọn một độ dài", + "select_your_email": "Chọn email của bạn", + "select_your_email_desc": "Thêm ví bằng tài khoản Google hoặc Apple của bạn", "selected_network": "Mạng đã chọn", "selected_network_only_supports_device": "Mạng được chọn hiện tại chỉ hỗ trợ {deviceType}", "self_troubleshooting": "Tự khắc phục sự cố", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "Đang phê duyệt...", "swap.btn_building": "Đang xây dựng đơn hàng...", "swap.ch_status_hold": "Liên hệ hỗ trợ", + "swap.current_token": "Token hiện tại", "swap.limit_amount": "Bán {num1} {fromToken} để nhận {num2} {toToken}", "swap.loading_content": "Đang tính toán giá tốt nhất...", "swap.native_token_max_tip": "Vui lòng giữ lại một lượng nhỏ token để trả phí mạng, nếu không giao dịch sẽ thất bại.", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "Dịch vụ được cung cấp bởi {accelerator}", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "Ghi chú", "tx_accelerate.speed_up_with_accelerator_dialog_title": "Tăng tốc với {accelerator}", + "ultra_fast_setup": "Thiết lập siêu nhanh", "update.all_other_apps_closed": "Tất cả các ứng dụng OneKey khác và công cụ nâng cấp web đều đã được đóng.", "update.all_other_apps_closed_emoji": "Tất cả các ứng dụng OneKey khác và công cụ nâng cấp web đều đã được đóng. 🆗", "update.all_updates_complete": "Tất cả các cập nhật đã hoàn tất 👏🏻", diff --git a/packages/shared/src/locale/json/zh_CN.json b/packages/shared/src/locale/json/zh_CN.json index 266cca4523ea..13368f8fc8af 100644 --- a/packages/shared/src/locale/json/zh_CN.json +++ b/packages/shared/src/locale/json/zh_CN.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "租赁能源", "bandwidth_energy.title": "带宽与能源", "bandwidth_energy.what_is_bandwidth_energy": "什么是Tron的带宽和能量?", + "beginner_friendly": "新手友好", "bip44__standard": "BIP44 标准", "bluetooth.disable_in_settings": "如果您只想使用 USB 而不想再次看到上述提示,请前往 OneKey 应用 > 设置 > 偏好设置,然后关闭蓝牙。", "bluetooth.disabled": "蓝牙已禁用", @@ -294,6 +295,8 @@ "communication.timeout": "通信超时", "confirm_exit_dialog_desc": "您确定要退出数据迁移过程吗?", "confirm_exit_dialog_title": "确认退出", + "confirm_your_pin": "确认您的 PIN 码", + "confirm_your_pin_desc": "如果您忘记此 PIN 码,您将无法在新设备上恢复您的钱包。", "connect_device_to_computer_via_usb": "通过 USB 将 {deviceLabel} 连接到您的电脑", "connect_with_qr_code": "使用二维码连接", "contact_us_instruction": "需要更多帮助吗?", @@ -306,6 +309,7 @@ "content__normal": "正常", "content__slow": "慢", "content__to": "至", + "continue_with_social_platform": "使用 {platform} 继续", "copy_address_modal_item_create_address_instruction": "创建地址", "copy_address_modal_title": "账户地址", "copy_anyway": "仍要复制", @@ -314,6 +318,8 @@ "count_assets": "{count} 种资产", "count_hidden_assets": "{count} 个隐藏的资产", "count_words": "{length} 个单词", + "create_a_pin": "创建 PIN 码", + "create_a_pin_desc": "用于在所有设备上保护你的钱包。此 PIN 无法找回。", "create_new_wallet_badge_consists": "助记词由 12 个单词组成", "create_new_wallet_badge_handwritten": "手写备份", "create_new_wallet_badge_keep": "需要自己妥善保管", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "最常使用", "create_new_wallet_badge_supports": "支持数百个网络", "create_new_wallet_learn_more": "助记词是您钱包安全的核心。它由 12 个常见的英文单词组成,用于创建和恢复您的私钥和钱包地址。请手写记录并妥善保管——只有您能访问您的资产。", + "create_passcode_desc": "您将使用此密码来解锁您的钱包。", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "在创建标准钱包后,输入一个 Passphrase 以创建一个隐藏钱包。", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "创建隐藏钱包", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "在显示二维码之前,无需输入 Passphrase。点击 ✅ 按钮,展示二维码,然后用应用程序扫描它以创建一个二维码标准钱包。", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "无效的 PIN 码", "enter_pin.title": "输入 PIN 码", "enter_pin_on_app": "在 app 输入 PIN 码", + "enter_your_pin": "输入您的 PIN 码", + "enter_your_pin_desc": "此邮箱已创建钱包。请输入您的 PIN 码以登录。", "explore.add_bookmark": "添加书签", "explore.add_to_whitelist": "添加到白名单", "explore.addresses_count": "{number}个地址", @@ -1072,6 +1081,7 @@ "for_large_assets": "适合大额资产", "for_reference_only": "仅供参考", "forgot_password_no_question_mark": "忘记密码", + "forgot_pin": "忘记 PIN 码?", "form.address_error_invalid": "无效地址", "form.address_placeholder": "地址或域名", "form.amount_placeholder": "输入金额", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "硬件通讯桥下载", "global.faqs_firmware_detection": "通信检测", "global.faqs_forgot_pin": "忘记 PIN", - "global.faqs_reset_wallet": "重置钱包", + "global.faqs_reset_device": "重置设备", "global.favorites": "自选", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "搜索账户名称", "global.search_address": "搜索地址", "global.search_asset": "搜索资产", + "global.search_everything": "搜索全部", "global.search_no_results_desc": "尝试更换搜索关键词", "global.search_no_results_title": "无结果", + "global.search_placeholder_web": "搜索", "global.search_tokens": "搜索代币", "global.secure_install": "安全安装", "global.security": "安全", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "观察钱包", "global.watched": "观察钱包", "global.watchlist": "观察列表", + "global.web_access_for_hardware_wallet_disconnected": "硬件钱包连接的网页访问已停止使用。已连接的设备仍可继续使用;如果您遇到问题,请使用 App 或浏览器扩展程序。", "global.web_feature_not_available_go_to_app": "Web 端暂不支持该功能,请在客户端中使用。", "global.website": "网站", "global.what_happen": "将会发生什么:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "请输入此备份的密码。", "import_hardware_phrases_warning": "不要导入硬件钱包的助记词,请使用连接硬件钱包 ↗", "import_phrase_or_private_key": "导入助记词或私钥", + "incorrect_pin": "PIN 码错误。请重试。", "insufficient_fee_append_desc": "基于最大預估费用:{amount} {symbol}", "interact_with_contract": "交互地址 (至)", "kaspa_official": "Kaspa 官方", + "keyless_wallet": "无私钥钱包", + "keyless_wallet_desc": "您的无私钥钱包安全地分散存储在多个因素中。通过您的 Google 或 Apple 账户和 4 位数 PIN 码即可恢复对钱包的访问权限。", "learn_how_to_withdraw_crypto_from_exchange": "了解如何从这些交易所提取加密资产到 OneKey", "learn_more_about_qr_code_wallet": "了解更多关于二维码钱包的信息", "lightning_invoice": "闪电发票", @@ -2044,6 +2060,7 @@ "market.ath_desc": "{token} 的历史最高点是在 {time},价格为 {price},当前价格比该高点下降了 {percent}。", "market.atl_desc": "{token}的历史最低价出现在{time} ,价格为{price} ,当前价格较最低价上涨了{percent} 。", "market.cex": "CEX", + "market.change_24h": "24h 涨跌", "market.chart": "图表", "market.days_since_launch": "上线时间", "market.empty_watchlist_desc": "将您最喜欢的代币添加到观察列表", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "所选账户不可用的网络", "network_show_enabled_only": "仅显示已启用的网络", "network_visible_in_all_network_tooltip_title": "显示于「所有网络」视图中", + "new_pin_created": "已创建新 PIN 码", + "new_pin_created_desc": "您可以随时在「设置」中修改 PIN。", "nft.already_collected": "当前 NFT 已收藏。", "nft.attributes": "属性", "nft.collect_failed": "收藏失败,请再试一次。", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "侧边栏模式", "open_in_mobile_app": "在移动端 App 中打开", "open_ordinals_transfer_tutorial_url_message": "如何转移 Ordinals 资产?", + "open_source_secure_sharding": "开源安全加密", "p2pkh_desc": "以“1”开头。P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "以“bc1q”开头。P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "以“bc1p”开头。P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "需要更多 {token} ?", "perps.get_reward": "获得奖励", "perps.offline_moblie": "连接中断,请检查您的网络并尝试下拉刷新", + "perps.settings_return_to_default_layout": "返回默认布局", "perps.share_position_background": "背景图片", "perps.share_position_btn_Share_on_x": "在 X 上分享", "perps.share_position_btn_copy_link": "复制链接", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "股票", "perps.trade_reward": "交易奖励", "pick_your_device": "选择您的设备", + "pin_attempts_cooldown": "请在 {seconds} 后再试。", + "pin_attempts_remaining": "输入的 PIN 码不正确。剩余 ${attemptsRemaining} 次尝试机会。", "preparing_backup_desc": "请稍候…", "preparing_backup_title": "正在准备备份…", "prime.about_cloud_sync": "关于云端同步", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "使用您的钱包地址接收", "receive_from_exchange": "从交易所接收", "receive_token_list_footer_text": "找不到代币?试试搜索", + "recovery_phrase_free": "无需助记词", "recovery_phrase_screenshot_protected_desc": "为了您的资产安全,您的助记词将永远不会出现在截图中。", "recovery_phrase_screenshot_protected_title": "助记词已受保护", "referral.accept": "接受", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} 个地址", "referral_code_tutorial_label": "如何获取邀请码?", "referral_promo_title": "加入 OneKey 推荐计划", + "remember_your_pin": "记得您的 PIN 码吗?", + "remember_your_pin_desc": "只是温馨提醒您,保持对 PIN 的记忆。我们会不定期确认。", "remove_account_desc": "此账户将被删除。", "remove_device": "移除设备", "remove_device_desc": "这将断开此硬件钱包与 OneKey App 的连接。您可以随时重新连接到 App。", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "在删除钱包之前,请确保您已写下助记词。否则,您将无法恢复钱包。", "remove_wallet_double_confirm_message": "我已经写下了助记词", "reset_app_desc": "这将删除您在 OneKey 上创建的所有数据。确保您已妥善备份,输入「RESET」以重置 app。", + "reset_pin": "重置 PIN 码", "scan.camera_access_denied": "没有摄像头访问权限", "scan.enable_camera_permissions": "OneKey 需要使用摄像头来扫描二维码,请前往“设置”并启用摄像头权限以使用此功能。", "scan.grant_camera_access_in_expand_view": "请在展开视图中授权相机访问", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "扫描以创建地址", "scanning_text": "扫描", "secure_qr_toast_scan_qr_code_on_device_text": "显示二维码后回到这里,点击「下一步」,然后扫描它。", + "seed_phrase_wallet": "助记词钱包", "select_connect_app_on_home": "在主屏幕选择连接 App 钱包", "select_onekey_app": "选择 OneKey App", "select_recovery_phrase_length": "助记词数量", + "select_your_email": "选择您的电子邮件地址", + "select_your_email_desc": "使用 Apple 账户或者 Google 账号添加钱包", "selected_network": "已选网络", "selected_network_only_supports_device": "所选网络目前仅支持 {deviceType}", "self_troubleshooting": "自助排查", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "授权中...", "swap.btn_building": "创建订单中...", "swap.ch_status_hold": "联系客服", + "swap.current_token": "当前代币", "swap.limit_amount": "卖出 {num1} {fromToken} 购买 {num2} {toToken}", "swap.loading_content": "正在计算最佳价格...", "swap.native_token_max_tip": "请保留少量代币用于支付网络费用,否则交易将会失败。", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "服务由 {accelerator} 提供", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "注意", "tx_accelerate.speed_up_with_accelerator_dialog_title": "使用{accelerator}加速", + "ultra_fast_setup": "极速上手", "update.all_other_apps_closed": "所有其他的 OneKey 应用程序和网页工具都已关闭", "update.all_other_apps_closed_emoji": "所有其他的 OneKey 应用程序和网页升级工具都已关闭 🆗", "update.all_updates_complete": "所有更新已完成👏🏻", diff --git a/packages/shared/src/locale/json/zh_HK.json b/packages/shared/src/locale/json/zh_HK.json index 55cb76a4b526..c7e74e5904e5 100644 --- a/packages/shared/src/locale/json/zh_HK.json +++ b/packages/shared/src/locale/json/zh_HK.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "租賃能源", "bandwidth_energy.title": "寬頻與能源", "bandwidth_energy.what_is_bandwidth_energy": "什麼是Tron的頻寬和能量?", + "beginner_friendly": "新手友好", "bip44__standard": "BIP44 標準", "bluetooth.disable_in_settings": "如果你偏好只使用 USB 而不想再次看到此提示,請前往 OneKey 應用程式 > 設定 > 偏好設定,然後關閉藍牙功能。", "bluetooth.disabled": "藍牙已停用", @@ -294,6 +295,8 @@ "communication.timeout": "通訊超時", "confirm_exit_dialog_desc": "您確定要退出數據遷移過程嗎?", "confirm_exit_dialog_title": "確認退出", + "confirm_your_pin": "確認您的 PIN 碼", + "confirm_your_pin_desc": "如果您忘記此 PIN 碼,您將無法在新裝置上復原您的錢包。", "connect_device_to_computer_via_usb": "透過 USB 將 {deviceLabel} 連接到你的電腦", "connect_with_qr_code": "使用二維碼連接", "contact_us_instruction": "需要更多幫助嗎?", @@ -306,6 +309,7 @@ "content__normal": "正常", "content__slow": "慢速", "content__to": "至", + "continue_with_social_platform": "使用 {platform} 繼續", "copy_address_modal_item_create_address_instruction": "建立地址", "copy_address_modal_title": "帳戶地址", "copy_anyway": "仍要複製", @@ -314,6 +318,8 @@ "count_assets": "{count} 種資產", "count_hidden_assets": "{count} 個隱藏的資產", "count_words": "{length} 個單詞", + "create_a_pin": "建立 PIN 碼", + "create_a_pin_desc": "用於在所有裝置上保護你的錢包。此 PIN 無法找回。", "create_new_wallet_badge_consists": "助記詞由 12 個單詞組成", "create_new_wallet_badge_handwritten": "手寫備份", "create_new_wallet_badge_keep": "需要自己妥善保管", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "最常使用", "create_new_wallet_badge_supports": "支援數百個網絡", "create_new_wallet_learn_more": "助記詞是您錢包安全的核心。它由 12 個常見的英文單詞組成,用於創建和恢復您的私鑰和錢包地址。請手寫記錄並妥善保管——只有您才能存取您的資產。", + "create_passcode_desc": "你將使用此來解鎖你的錢包。", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "在創建標準錢包後,輸入一個 Passphrase 以創建一個隱藏錢包。", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "創建隱藏錢包", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "在顯示二維碼之前,無需輸入 Passphrase。點擊 ✅ 按鈕,展示二維碼,然後用應用程式掃描它以建立一個二維碼標準錢包。", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "無效的 PIN 碼", "enter_pin.title": "輸入 PIN 碼", "enter_pin_on_app": "在 app 輸入 PIN 碼", + "enter_your_pin": "輸入您的 PIN 碼", + "enter_your_pin_desc": "此電郵地址已建立錢包。請輸入您的 PIN 碼以登入。", "explore.add_bookmark": "加入書籤", "explore.add_to_whitelist": "添加到白名單", "explore.addresses_count": "{number}個地址", @@ -1072,6 +1081,7 @@ "for_large_assets": "適合大額資產", "for_reference_only": "僅供參考", "forgot_password_no_question_mark": "忘記密碼", + "forgot_pin": "忘記 PIN 碼?", "form.address_error_invalid": "無效地址", "form.address_placeholder": "地址或域名", "form.amount_placeholder": "輸入金額", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "硬件通信橋下載", "global.faqs_firmware_detection": "通訊檢測", "global.faqs_forgot_pin": "忘記 PIN", - "global.faqs_reset_wallet": "重置錢包", + "global.faqs_reset_device": "重設裝置", "global.favorites": "自選", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "搜尋帳戶名稱", "global.search_address": "搜尋地址", "global.search_asset": "搜尋資產", + "global.search_everything": "搜尋全部", "global.search_no_results_desc": "嘗試更換搜索關鍵詞", "global.search_no_results_title": "無結果", + "global.search_placeholder_web": "搜尋", "global.search_tokens": "搜索代幣", "global.secure_install": "安全安裝", "global.security": "安全", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "觀察錢包", "global.watched": "觀察帳戶", "global.watchlist": "觀察清單", + "global.web_access_for_hardware_wallet_disconnected": "硬件錢包連接的網頁訪問已停止使用。已連接的設備仍可繼續使用;如遇到問題,請使用 App 或瀏覽器擴充功能。", "global.web_feature_not_available_go_to_app": "Web 端暫不支持該功能,請在客戶端中使用。", "global.website": "網站", "global.what_happen": "將會發生什麼:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "請輸入此備份的密碼。", "import_hardware_phrases_warning": "不要導入硬體錢包的助記詞,請使用連接硬件錢包 ↗", "import_phrase_or_private_key": "匯入助記詞或私鑰", + "incorrect_pin": "PIN 碼錯誤。請重試。", "insufficient_fee_append_desc": "基於最大估計費用:{amount} {symbol}", "interact_with_contract": "互動地址 (至)", "kaspa_official": "Kaspa 官方", + "keyless_wallet": "無私鑰錢包", + "keyless_wallet_desc": "您的無私鑰錢包安全地分散儲存於多個因素中。透過您的 Google 或 Apple 帳戶及 4 位數字 PIN 碼即可恢復存取您的錢包。", "learn_how_to_withdraw_crypto_from_exchange": "了解如何從這些交易所提取加密資產到 OneKey", "learn_more_about_qr_code_wallet": "了解更多關於二維碼錢包的信息", "lightning_invoice": "閃電發票", @@ -2044,6 +2060,7 @@ "market.ath_desc": "{token} 的歷史最高點是在 {time},價格為 {price},目前價格比該高點下降了 {percent}。", "market.atl_desc": "{token}的歷史最低價為{time} ,價格為{price} ,目前價格較該最低價上漲了{percent} 。", "market.cex": "CEX", + "market.change_24h": "24h 漲跌", "market.chart": "圖表", "market.days_since_launch": "上線時間", "market.empty_watchlist_desc": "將您最喜歡的代幣添加到觀察列表", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "所選帳戶的網路不可用", "network_show_enabled_only": "只顯示已啟用的網絡", "network_visible_in_all_network_tooltip_title": "顯示於「所有網絡」視圖", + "new_pin_created": "已建立新 PIN 碼", + "new_pin_created_desc": "您可以隨時在「設定」中修改 PIN。", "nft.already_collected": "當前 NFT 已收藏。", "nft.attributes": "屬性", "nft.collect_failed": "收藏失敗,請再試一次。", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "側邊欄模式", "open_in_mobile_app": "在移動端 App 中打開", "open_ordinals_transfer_tutorial_url_message": "如何轉移 Ordinals 資產?", + "open_source_secure_sharding": "開源安全加密", "p2pkh_desc": "以「1」開頭。P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "以「bc1q」開頭。P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "以\"bc1p\"開頭。P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "需要更多 {token} ?", "perps.get_reward": "獲得獎勵", "perps.offline_moblie": "連線中斷。請檢查您的網絡並嘗試下拉刷新", + "perps.settings_return_to_default_layout": "返回預設佈局", "perps.share_position_background": "背景图片", "perps.share_position_btn_Share_on_x": "在 X 上分享", "perps.share_position_btn_copy_link": "複製連結", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "股票", "perps.trade_reward": "交易獎勵", "pick_your_device": "選擇你的裝置", + "pin_attempts_cooldown": "請在 {seconds} 後再試。", + "pin_attempts_remaining": "輸入的 PIN 碼不正確。剩餘 {attemptsRemaining} 次嘗試機會。", "preparing_backup_desc": "請稍候…", "preparing_backup_title": "正在準備備份…", "prime.about_cloud_sync": "關於雲端同步", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "使用您的錢包地址接收", "receive_from_exchange": "從交易所接收", "receive_token_list_footer_text": "找不到代幣?試試搜索", + "recovery_phrase_free": "無需助記詞", "recovery_phrase_screenshot_protected_desc": "為了您的資產安全,您的助記詞將永遠不會出現在螢幕截圖中。", "recovery_phrase_screenshot_protected_title": "助記詞已受保護", "referral.accept": "接受", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} 個地址", "referral_code_tutorial_label": "如何取得邀請碼?", "referral_promo_title": "加入 OneKey 推薦計劃", + "remember_your_pin": "記得您的 PIN 碼嗎?", + "remember_your_pin_desc": "只是溫馨提醒您,保持對 PIN 的記憶。我們會不定期確認。", "remove_account_desc": "此帳戶將被刪除。", "remove_device": "移除裝置", "remove_device_desc": "這將會將此硬件錢包從 OneKey App 中斷開。您可以隨時重新連接到 App。", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "在刪除錢包之前,請確保你已寫下助記詞。否則,你將無法恢復錢包。", "remove_wallet_double_confirm_message": "我已經寫下了助記詞", "reset_app_desc": "這將刪除您在 OneKey 上創建的所有數據。確保您已妥善備份,輸入「RESET」以重置 app。", + "reset_pin": "重設 PIN 碼", "scan.camera_access_denied": "沒有攝像頭訪問權限", "scan.enable_camera_permissions": "OneKey 需要使用攝像頭來掃描二維碼,請前往“設置”並啓用攝像頭權限以使用此功能。", "scan.grant_camera_access_in_expand_view": "請在展開視圖中授予相機訪問權限", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "掃描以創建地址", "scanning_text": "掃描", "secure_qr_toast_scan_qr_code_on_device_text": "顯示二維碼後回到這裡,點擊「下一步」,然後掃描它。", + "seed_phrase_wallet": "助記詞錢包", "select_connect_app_on_home": "在主畫面選擇連接 App 錢包", "select_onekey_app": "選擇 OneKey App", "select_recovery_phrase_length": "助記詞數量", + "select_your_email": "選擇您的電子郵件地址", + "select_your_email_desc": "使用 Apple 帳戶或 Google 帳號新增錢包", "selected_network": "已選擇的網絡", "selected_network_only_supports_device": "所選的網路目前只支援 {deviceType}", "self_troubleshooting": "自助排查", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "授權中...", "swap.btn_building": "建立訂單中...", "swap.ch_status_hold": "聯繫支援人員", + "swap.current_token": "目前代幣", "swap.limit_amount": "賣出 {num1} {fromToken} 買入 {num2} {toToken}", "swap.loading_content": "計算最佳價格中...", "swap.native_token_max_tip": "請保留少量代幣以支付網絡費用,否則交易將會失敗。", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "服務由 {accelerator} 提供", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "注意", "tx_accelerate.speed_up_with_accelerator_dialog_title": "加速器 {accelerator} 提速", + "ultra_fast_setup": "極速上手", "update.all_other_apps_closed": "所有其他 OneKey 應用程式和網頁升級工具均已關閉", "update.all_other_apps_closed_emoji": "所有其他 OneKey 應用程式和網頁升級工具均已關閉 🆗", "update.all_updates_complete": "全部更新完畢👏🏻", diff --git a/packages/shared/src/locale/json/zh_TW.json b/packages/shared/src/locale/json/zh_TW.json index 0c8098f857e2..22845eddc45a 100644 --- a/packages/shared/src/locale/json/zh_TW.json +++ b/packages/shared/src/locale/json/zh_TW.json @@ -240,6 +240,7 @@ "bandwidth_energy.rent_energy": "租用能源", "bandwidth_energy.title": "頻寬與能源", "bandwidth_energy.what_is_bandwidth_energy": "什麼是Tron的頻寬和能量?", + "beginner_friendly": "新手友善", "bip44__standard": "BIP44 標準", "bluetooth.disable_in_settings": "如果您偏好僅使用 USB 且不想再次看到此提示,請前往 OneKey 應用程式 > 設定 > 偏好設定,並關閉藍牙功能。", "bluetooth.disabled": "藍牙已停用", @@ -294,6 +295,8 @@ "communication.timeout": "通訊超時", "confirm_exit_dialog_desc": "您確定要退出數據遷移過程嗎?", "confirm_exit_dialog_title": "確認退出", + "confirm_your_pin": "確認您的 PIN 碼", + "confirm_your_pin_desc": "如果您忘記此 PIN 碼,您將無法在新裝置上恢復您的錢包。", "connect_device_to_computer_via_usb": "透過 USB 將 {deviceLabel} 連接到您的電腦", "connect_with_qr_code": "使用 QR Code 連線", "contact_us_instruction": "需要更多幫助嗎?", @@ -306,6 +309,7 @@ "content__normal": "正常", "content__slow": "慢", "content__to": "至", + "continue_with_social_platform": "使用 {platform} 繼續", "copy_address_modal_item_create_address_instruction": "建立地址", "copy_address_modal_title": "帳戶地址", "copy_anyway": "仍然複製", @@ -314,6 +318,8 @@ "count_assets": "{count} 種資產", "count_hidden_assets": "{count} 隱藏的資產", "count_words": "{length} 個字", + "create_a_pin": "建立 PIN 碼", + "create_a_pin_desc": "用於在所有裝置上保護你的錢包。此 PIN 無法找回。", "create_new_wallet_badge_consists": "助記詞由 12 個單詞組成", "create_new_wallet_badge_handwritten": "手寫備份", "create_new_wallet_badge_keep": "需要自己妥善保管", @@ -321,6 +327,7 @@ "create_new_wallet_badge_most_used": "最常使用", "create_new_wallet_badge_supports": "支援數百個網路", "create_new_wallet_learn_more": "助記詞是您錢包安全的核心。它由 12 個常見的英文單字組成,用於建立和還原您的私鑰和錢包地址。請手寫記錄並妥善保管——只有您能存取您的資產。", + "create_passcode_desc": "您將使用此來解鎖您的錢包。", "create_qr_based_hidden_wallet_create_hidden_wallet_desc": "在創建標準錢包後,輸入一個 Passphrase 以創建一個隱藏錢包。", "create_qr_based_hidden_wallet_create_hidden_wallet_title": "創建隱藏錢包", "create_qr_based_hidden_wallet_create_standard_wallet_desc": "在顯示二維碼之前,無需輸入 Passphrase。點擊 ✅ 按鈕,展示二維碼,然後用應用程式掃描它以建立一個二維碼標準錢包。", @@ -866,6 +873,8 @@ "enter_pin.invalid_pin": "無效的 PIN 碼", "enter_pin.title": "輸入 PIN 碼", "enter_pin_on_app": "在 app 輸入 PIN 碼", + "enter_your_pin": "輸入您的 PIN 碼", + "enter_your_pin_desc": "此電子郵件已建立錢包。請輸入您的 PIN 碼以登入。", "explore.add_bookmark": "新增書籤", "explore.add_to_whitelist": "加入白名單", "explore.addresses_count": "{number} 個地址", @@ -1072,6 +1081,7 @@ "for_large_assets": "適合大額資產", "for_reference_only": "僅供參考", "forgot_password_no_question_mark": "忘記密碼", + "forgot_pin": "忘記 PIN 碼?", "form.address_error_invalid": "無效地址", "form.address_placeholder": "地址或域名", "form.amount_placeholder": "輸入金額", @@ -1367,7 +1377,7 @@ "global.faqs_bridge_download": "硬體通信橋下載", "global.faqs_firmware_detection": "通訊檢測", "global.faqs_forgot_pin": "忘記 PIN", - "global.faqs_reset_wallet": "重置錢包", + "global.faqs_reset_device": "重設裝置", "global.favorites": "自選", "global.fcc_id": "FCC ID", "global.fdv": "FDV", @@ -1674,8 +1684,10 @@ "global.search_account_selector": "搜尋帳戶名稱", "global.search_address": "搜尋地址", "global.search_asset": "搜尋資產", + "global.search_everything": "搜尋全部", "global.search_no_results_desc": "嘗試更改搜尋關鍵字", "global.search_no_results_title": "無結果", + "global.search_placeholder_web": "搜索", "global.search_tokens": "搜索代幣", "global.secure_install": "安全安裝", "global.security": "安全", @@ -1814,6 +1826,7 @@ "global.watch_only_wallet": "觀察錢包", "global.watched": "觀察帳戶", "global.watchlist": "觀察清單", + "global.web_access_for_hardware_wallet_disconnected": "硬體錢包連接的網頁存取已停止提供。已連接的裝置仍可繼續使用;如果您遇到問題,請使用 App 或瀏覽器擴充功能。", "global.web_feature_not_available_go_to_app": "Web 端暫不支持該功能,請在客戶端中使用。", "global.website": "網站", "global.what_happen": "將會發生什麼:", @@ -1959,9 +1972,12 @@ "import_backup_password_desc": "請輸入此備份的密碼。", "import_hardware_phrases_warning": "不要導入硬體錢包的助記詞,請使用連接硬體錢包 ↗", "import_phrase_or_private_key": "匯入助記詞或私鑰", + "incorrect_pin": "PIN 碼錯誤。請重試。", "insufficient_fee_append_desc": "基於最大估計費用:{amount} {symbol}", "interact_with_contract": "互動地址 (至)", "kaspa_official": "Kaspa 官方", + "keyless_wallet": "無私鑰錢包", + "keyless_wallet_desc": "您的無私鑰錢包安全地分散儲存在多個因素中。使用您的 Google 或 Apple 帳戶和 4 位數 PIN 碼即可恢復對錢包的存取權限。", "learn_how_to_withdraw_crypto_from_exchange": "了解如何從這些交易所提領加密資產至 OneKey", "learn_more_about_qr_code_wallet": "了解更多關於二維碼錢包的信息", "lightning_invoice": "閃電發票", @@ -2044,6 +2060,7 @@ "market.ath_desc": "{token} 的歷史最高點是在 {time},價格為 {price},目前價格比該高點下降了 {percent}。", "market.atl_desc": "{token}的歷史最低價為{time} ,價格為{price} ,目前價格較該最低價上漲了{percent} 。", "market.cex": "CEX", + "market.change_24h": "24h 漲跌", "market.chart": "圖表", "market.days_since_launch": "上線時間", "market.empty_watchlist_desc": "將您最喜歡的代幣添加到觀察列表", @@ -2147,6 +2164,8 @@ "network_selector.unavailable_networks": "所選帳戶的網路不可用", "network_show_enabled_only": "僅顯示已啟用的網路", "network_visible_in_all_network_tooltip_title": "顯示於「所有網路」視圖中", + "new_pin_created": "已建立新 PIN 碼", + "new_pin_created_desc": "您可以隨時在「設定」中修改 PIN。", "nft.already_collected": "當前 NFT 已收藏。", "nft.attributes": "屬性", "nft.collect_failed": "收藏失敗,請再試一次。", @@ -2280,6 +2299,7 @@ "open_as_sidebar": "側邊欄模式", "open_in_mobile_app": "在手機 App 中開啟", "open_ordinals_transfer_tutorial_url_message": "如何轉移 Ordinals 資產?", + "open_source_secure_sharding": "開源安全加密", "p2pkh_desc": "開始於 \"1\"。P2PKH (m/44'/0'/0')", "p2sh_p2wpkh_desc": "以“bc1q”開頭。P2SH-P2WPKH (m/84'/0'/0')", "p2tr_desc": "以“bc1p”開頭。P2TR (m/86'/0'/0')", @@ -2564,6 +2584,7 @@ "perps.buy_tip": "需要更多 {token} ?", "perps.get_reward": "獲得獎勵", "perps.offline_moblie": "連線中斷。請檢查您的網路並嘗試下拉刷新", + "perps.settings_return_to_default_layout": "返回預設佈局", "perps.share_position_background": "背景图片", "perps.share_position_btn_Share_on_x": "在 X 上分享", "perps.share_position_btn_copy_link": "複製連結", @@ -2574,6 +2595,8 @@ "perps.token_selector_stocks": "股票", "perps.trade_reward": "交易獎勵", "pick_your_device": "選擇您的裝置", + "pin_attempts_cooldown": "請在 {seconds} 後再試。", + "pin_attempts_remaining": "輸入的 PIN 碼不正確。剩餘 ${attemptsRemaining} 次嘗試機會。", "preparing_backup_desc": "請稍候…", "preparing_backup_title": "正在準備備份…", "prime.about_cloud_sync": "關於雲端同步", @@ -2826,6 +2849,7 @@ "receive_from_another_wallet_desc": "使用您的錢包地址接收", "receive_from_exchange": "從交易所接收", "receive_token_list_footer_text": "找不到代幣?試試搜尋", + "recovery_phrase_free": "無需助記詞", "recovery_phrase_screenshot_protected_desc": "為了您的資產安全,您的助記詞將永遠不會出現在螢幕截圖中。", "recovery_phrase_screenshot_protected_title": "助記詞已受保護", "referral.accept": "接受", @@ -3015,6 +3039,8 @@ "referral.your_referred_wallets_more_address": "{amount} 個地址", "referral_code_tutorial_label": "如何取得邀請碼?", "referral_promo_title": "加入 OneKey 推薦計畫", + "remember_your_pin": "記得您的 PIN 碼嗎?", + "remember_your_pin_desc": "只是溫馨提醒您,保持對 PIN 的記憶。我們會不定期確認。", "remove_account_desc": "此帳戶將被移除。", "remove_device": "移除裝置", "remove_device_desc": "這將會把這個硬體錢包從 OneKey App 中斷開。您可以隨時重新連接到 App。", @@ -3027,6 +3053,7 @@ "remove_wallet_desc": "在移除錢包之前,請確保您已寫下助記詞。否則,您將無法恢復錢包。", "remove_wallet_double_confirm_message": "我已經寫下了助記詞", "reset_app_desc": "這將刪除您在OneKey上創建的所有數據。確保您已經妥善備份後,輸入\"RESET\"以重置應用程式", + "reset_pin": "重設 PIN 碼", "scan.camera_access_denied": "沒有攝像頭訪問權限", "scan.enable_camera_permissions": "OneKey 需要使用攝像頭來掃描二維碼,請前往“設置”並啓用攝像頭權限以使用此功能。", "scan.grant_camera_access_in_expand_view": "請在展開視圖中授予相機訪問權限", @@ -3044,9 +3071,12 @@ "scan_to_create_an_address": "掃描以創建地址", "scanning_text": "掃描", "secure_qr_toast_scan_qr_code_on_device_text": "顯示二維碼後回到這裡,點擊「下一步」,然後掃描它。", + "seed_phrase_wallet": "助記詞錢包", "select_connect_app_on_home": "在主畫面選擇連接 App 錢包", "select_onekey_app": "選擇 OneKey App", "select_recovery_phrase_length": "選擇一個長度", + "select_your_email": "選擇您的電子郵件地址", + "select_your_email_desc": "使用 Apple 帳戶或 Google 帳號新增錢包", "selected_network": "已選取的網路", "selected_network_only_supports_device": "所選的網路目前只支援 {deviceType}", "self_troubleshooting": "自助排查", @@ -3366,6 +3396,7 @@ "swap.btn_approving": "授權中...", "swap.btn_building": "建立訂單中...", "swap.ch_status_hold": "聯繫客服", + "swap.current_token": "目前代幣", "swap.limit_amount": "賣出 {num1} {fromToken} 買入 {num2} {toToken}", "swap.loading_content": "正在計算最佳價格...", "swap.native_token_max_tip": "請保留少量代幣以支付網絡費用,否則交易將會失敗。", @@ -3735,6 +3766,7 @@ "tx_accelerate.speed_up_with_accelerator_dialog_note_service_provide_by": "服務由 {accelerator} 提供", "tx_accelerate.speed_up_with_accelerator_dialog_note_title": "注意", "tx_accelerate.speed_up_with_accelerator_dialog_title": "使用{accelerator}加速", + "ultra_fast_setup": "極速上手", "update.all_other_apps_closed": "所有其他的 OneKey 應用程式和網頁升級工具都已關閉", "update.all_other_apps_closed_emoji": "所有其他的 OneKey 應用程式和網頁升級工具都已關閉 🆗", "update.all_updates_complete": "所有更新已完成 👏🏻", From d560f20c9070cbc7c71024346e1d7664f2dcd4c3 Mon Sep 17 00:00:00 2001 From: morizon Date: Fri, 26 Dec 2025 21:01:42 +0800 Subject: [PATCH 27/66] feat: enhance Keyless Wallet V2 with social login support and onboarding improvements --- development/spellCheckerSkipWords.js | 2 + packages/kit-bg/src/dbs/local/LocalDbBase.ts | 2 + packages/kit-bg/src/dbs/local/types.ts | 1 + .../services/ServiceAccount/ServiceAccount.ts | 6 ++ .../ServiceKeylessWallet.ts | 99 ++++++++++++++++++- .../KeylessWallet/useKeylessWallet.tsx | 91 ++++++++++++++++- .../OneKeyAuth/supabase/useSupabaseAuth.tsx | 92 +++++++---------- .../components/OneKeyAuth/useOneKeyAuth.tsx | 3 + .../container/PasswordSetupContainer.tsx | 7 +- .../contexts/accountSelector/actions.tsx | 36 +++++++ .../Components/stories/OneKeyIDGallery.tsx | 23 +++-- .../Onboardingv2/pages/ConfirmPinPage.tsx | 55 ++++++++--- .../pages/CreateOrImportWallet.tsx | 6 +- .../Onboardingv2/pages/CreatePasscodePage.tsx | 21 ++-- .../Onboardingv2/pages/CreatePinPage.tsx | 27 ++++- .../Onboardingv2/pages/OneKeyIDLoginPage.tsx | 93 ++++++++++++++--- packages/shared/src/consts/authConsts.ts | 6 ++ .../src/keylessWallet/keylessWalletTypes.ts | 17 ++++ packages/shared/src/routes/onboardingv2.ts | 16 ++- 19 files changed, 487 insertions(+), 116 deletions(-) diff --git a/development/spellCheckerSkipWords.js b/development/spellCheckerSkipWords.js index 784d6540b315..d289331fdb98 100644 --- a/development/spellCheckerSkipWords.js +++ b/development/spellCheckerSkipWords.js @@ -16,6 +16,7 @@ module.exports = [ 'reown', 'cloudfs', 'getters', + 'Autopurge', 'keyless', 'shamir', '0xxxxxxx', @@ -29,6 +30,7 @@ module.exports = [ 'cloudkit', 'nktkas', 'bbo', + 'juicebox', '100vw', '10xxxxxx', 'hardcode', diff --git a/packages/kit-bg/src/dbs/local/LocalDbBase.ts b/packages/kit-bg/src/dbs/local/LocalDbBase.ts index 70d9cdee6e83..edb78459f172 100644 --- a/packages/kit-bg/src/dbs/local/LocalDbBase.ts +++ b/packages/kit-bg/src/dbs/local/LocalDbBase.ts @@ -1990,6 +1990,7 @@ export abstract class LocalDbBase extends LocalDbBaseContainer { rs, walletHash, walletXfp, + isKeylessWallet, } = params; const context = await this.getContext({ verifyPassword: password }); const walletId = accountUtils.buildHdWalletId({ @@ -2022,6 +2023,7 @@ export abstract class LocalDbBase extends LocalDbBaseContainer { accounts: [], walletNo: context.nextWalletNo, deprecated: false, + isKeyless: !!isKeylessWallet, }; currentWalletToCreate = _walletToCreate; currentAvatarInfo = options.avatar; diff --git a/packages/kit-bg/src/dbs/local/types.ts b/packages/kit-bg/src/dbs/local/types.ts index 4c1134caeae0..b669f7e135fe 100644 --- a/packages/kit-bg/src/dbs/local/types.ts +++ b/packages/kit-bg/src/dbs/local/types.ts @@ -176,6 +176,7 @@ export type IDBCreateHDWalletParams = { walletHash: string; walletXfp: string; avatar?: IAvatarInfo; + isKeylessWallet?: boolean; }; export type IDBCreateKeylessWalletParams = { password: string; diff --git a/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts b/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts index e0ae6b53927e..964a440706ca 100644 --- a/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts +++ b/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts @@ -3071,11 +3071,13 @@ class ServiceAccount extends ServiceBase { name, mnemonic, isWalletBackedUp, + isKeylessWallet, avatarInfo, }: { mnemonic: string; name?: string; isWalletBackedUp?: boolean; + isKeylessWallet?: boolean; avatarInfo?: IAvatarInfo; }) { const { servicePassword } = this.backgroundApi; @@ -3116,6 +3118,7 @@ class ServiceAccount extends ServiceBase { walletHash: walletHashAndXfp.hash, walletXfp: walletHashAndXfp.xfp, isWalletBackedUp, + isKeylessWallet, avatarInfo, }); } @@ -3162,6 +3165,7 @@ class ServiceAccount extends ServiceBase { walletHash, walletXfp, isWalletBackedUp, + isKeylessWallet, }: { rs: string; password: string; @@ -3170,6 +3174,7 @@ class ServiceAccount extends ServiceBase { walletHash: string; walletXfp: string; isWalletBackedUp?: boolean; + isKeylessWallet?: boolean; }): Promise<{ wallet: IDBWallet; indexedAccount?: IDBIndexedAccount; @@ -3225,6 +3230,7 @@ class ServiceAccount extends ServiceBase { name, walletHash, walletXfp, + isKeylessWallet, }); await timerUtils.wait(100); diff --git a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts index 04df6de5fb87..8394860f629b 100644 --- a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts +++ b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts @@ -1,6 +1,12 @@ import { isEqual } from 'lodash'; +import { LRUCache } from 'lru-cache'; -import { mnemonicToEntropy } from '@onekeyhq/core/src/secret'; +import { + encryptAsync, + encryptStringAsync, + generateMnemonic, + mnemonicToEntropy, +} from '@onekeyhq/core/src/secret'; import { backgroundClass, backgroundMethod, @@ -14,16 +20,19 @@ import type { IAuthKeyPack, ICloudKeyPack, IDeviceKeyPack, + IKeylessBackendShare, IKeylessMnemonicInfo, IKeylessWalletPacks, IKeylessWalletRestoredData, IKeylessWalletUserInfo, + ISupabaseJWTPayload, } from '@onekeyhq/shared/src/keylessWallet/keylessWalletTypes'; import keylessWalletUtils from '@onekeyhq/shared/src/keylessWallet/keylessWalletUtils'; import shamirUtils from '@onekeyhq/shared/src/keylessWallet/shamirUtils'; import { appLocale } from '@onekeyhq/shared/src/locale/appLocale'; import { ETranslations } from '@onekeyhq/shared/src/locale/enum/translations'; import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; +import bufferUtils from '@onekeyhq/shared/src/utils/bufferUtils'; import type { IAvatarInfo } from '@onekeyhq/shared/src/utils/emojiUtils'; import { findMismatchedPaths } from '@onekeyhq/shared/src/utils/miscUtils'; import stringUtils from '@onekeyhq/shared/src/utils/stringUtils'; @@ -184,7 +193,6 @@ class ServiceKeylessWallet extends ServiceBase { restoreAuthPackFromServer: true, }); if (!result?.packs?.mnemonic) { - // TODO i18n @franco 无法启用无私钥钱包 throw new OneKeyLocalError('核验身份失败,无法启用您的无私钥钱包'); } return { @@ -1072,6 +1080,93 @@ class ServiceKeylessWallet extends ServiceBase { // do nothing } } + + @backgroundMethod() + @toastIfError() + async apiGetKeylessBackendShare(params: { + token: string; + }): Promise { + // verify token by supabase SDK, make sure token is valid and generated by Google or Apple + // decode token to get social account id + // hash social account id by env salt and KMS + const socialAccountId = ''; + return null; + } + + @backgroundMethod() + @toastIfError() + async apiUploadKeylessBackendShare(params: { + token: string; + encryptedMnemonic: string; + backendShare: string; + }): Promise { + const { token, encryptedMnemonic, backendShare } = params; + const decodedToken = stringUtils.decodeJWT(token) as ISupabaseJWTPayload; + const provider = decodedToken?.app_metadata?.provider || ''; + const socialAccountId = decodedToken?.user_metadata?.sub || ''; + const ownerId = `${provider}:${socialAccountId}`; + return { + ownerId, + encryptedMnemonic, + backendShare, + }; + } + + async apiUploadKeylessJuiceboxShare(params: { + token: string; + juiceboxShare: string; + }): Promise<{ success: boolean }> { + // exchange juicebox token from onekey auth server + // upload juicebox share to juicebox network + return { + success: true, + }; + } + + @backgroundMethod() + @toastIfError() + async createKeylessWalletV2(params: { token: string }) { + const { token } = params; + const mnemonic: string = generateMnemonic(256); + const mnemonicPasswordBytes = crypto.getRandomValues(new Uint8Array(32)); + const mnemonicPassword = bufferUtils.bytesToBase64(mnemonicPasswordBytes); + const encryptedMnemonic: string = await encryptStringAsync({ + data: mnemonic, + dataEncoding: 'utf-8', + password: mnemonicPassword, + allowRawPassword: true, + iterations: 600_000, + }); + const mnemonicPasswordShares = await shamirUtils.split( + new Uint8Array(mnemonicPasswordBytes), + 2, + 2, + ); + const [mnemonicPasswordShare1, mnemonicPasswordShare2] = + mnemonicPasswordShares; + const backendShare: string = bufferUtils.bytesToBase64( + mnemonicPasswordShare1, + ); + const juiceboxShare: string = bufferUtils.bytesToBase64( + mnemonicPasswordShare2, + ); + + const backendShareData: IKeylessBackendShare = + await this.apiUploadKeylessBackendShare({ + token, + encryptedMnemonic, + backendShare, + }); + const juiceboxShareData: { success: boolean } = + await this.apiUploadKeylessJuiceboxShare({ + token, + juiceboxShare, + }); + return this.backgroundApi.serviceAccount.createHDWallet({ + mnemonic, + isWalletBackedUp: true, + }); + } } export default ServiceKeylessWallet; diff --git a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx index 01267ef2001c..f252593e9034 100644 --- a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx +++ b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx @@ -22,9 +22,12 @@ import { EModalRoutes, ERootRoutes } from '@onekeyhq/shared/src/routes'; import { EOnboardingPagesV2, EOnboardingV2KeylessWalletCreationMode, + EOnboardingV2OneKeyIDLoginMode, EOnboardingV2Routes, } from '@onekeyhq/shared/src/routes/onboardingv2'; import { EPrimePages } from '@onekeyhq/shared/src/routes/prime'; +import cacheUtils from '@onekeyhq/shared/src/utils/cacheUtils'; +import timerUtils from '@onekeyhq/shared/src/utils/timerUtils'; import { EPrimeTransferDataType } from '@onekeyhq/shared/types/prime/primeTransferTypes'; import backgroundApiProxy from '../../background/instance/backgroundApiProxy'; @@ -270,6 +273,27 @@ export function useKeylessWalletMethods() { }; } +const keylessOnboardingCache = new cacheUtils.LRUCache({ + max: 1000, + ttl: timerUtils.getTimeDurationMs({ minute: 3 }), + ttlAutopurge: true, +}); + +function keylessOnboardingCacheGetAndDelete(key: string) { + const token = keylessOnboardingCache.get(key); + keylessOnboardingCache.delete(key); + return token; +} + +function keylessOnboardingCacheSet(key: string, value: string) { + keylessOnboardingCache.set(key, value); +} + +if (process.env.NODE_ENV !== 'production') { + // @ts-ignore + globalThis.$$keylessOnboardingCache = keylessOnboardingCache; +} + export function useKeylessWallet() { const methods = useKeylessWalletMethods(); const actions = useAccountSelectorActions(); @@ -411,7 +435,7 @@ export function useKeylessWallet() { ], ); - const enableKeylessWalletV2 = useCallback(async () => { + const checkKeylessWalletExistence = useCallback(async () => { if (enableKeylessWalletLoadingRef.current) { return; } @@ -425,6 +449,7 @@ export function useKeylessWallet() { if (exists) { Dialog.show({ title: 'Keyless Wallet', + // TODO @franco 本地已经添加无私钥钱包,如果需要使用其他无私钥钱包,请先删除当前钱包 description: 'You already have a Keyless Wallet on this device. No need to create another one.', showCancelButton: false, @@ -437,6 +462,9 @@ export function useKeylessWallet() { screen: EOnboardingV2Routes.OnboardingV2, params: { screen: EOnboardingPagesV2.OneKeyIDLogin, + params: { + mode: EOnboardingV2OneKeyIDLoginMode.CreateKeylessWallet, + }, }, }); } @@ -446,11 +474,70 @@ export function useKeylessWallet() { }); }, [intl, navigation]); + const checkKeylessWalletCreatedOnServer = useCallback( + async ({ token }: { token: string }) => { + const backendShareInfo = + await backgroundApiProxy.serviceKeylessWallet.apiGetKeylessBackendShare( + { + token, + }, + ); + return { + isCreated: !!backendShareInfo, + backendShareInfo, + }; + }, + [], + ); + + const createOrRestoreKeylessWallet = useCallback( + async ({ token }: { token: string }) => { + const { isCreated, backendShareInfo } = + await checkKeylessWalletCreatedOnServer({ token }); + if (isCreated) { + // TODO restore keyless wallet from server + console.log('backendShareInfo', backendShareInfo); + } else { + await actions.current.createKeylessWalletV2({ + token, + }); + } + }, + [actions, checkKeylessWalletCreatedOnServer], + ); + + const cacheKeylessOnboardingToken = useCallback( + async ({ token }: { token: string }) => { + keylessOnboardingCacheSet('socialLoginToken', token); + }, + [], + ); + const getKeylessOnboardingToken = useCallback(async () => { + const token = keylessOnboardingCacheGetAndDelete('socialLoginToken'); + return token; + }, []); + + const cacheKeylessOnboardingPin = useCallback(({ pin }: { pin: string }) => { + keylessOnboardingCacheSet('onboardingPin', pin); + }, []); + + const getKeylessOnboardingPin = useCallback(() => { + const pin = keylessOnboardingCacheGetAndDelete('onboardingPin'); + return pin; + }, []); + return { ...methods, // TODO handleKeylessWalletClick enableKeylessWallet, - enableKeylessWalletV2, + checkKeylessWalletExistence, + checkKeylessWalletCreatedOnServer, + createOrRestoreKeylessWallet, enableKeylessWalletLoading, + keylessOnboardingCache, + cacheKeylessOnboardingToken, + getKeylessOnboardingToken, + cacheKeylessOnboardingPin, + getKeylessOnboardingPin, }; } diff --git a/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx b/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx index 456f7ab0b68f..4ece241ab1b5 100644 --- a/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx +++ b/packages/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth.tsx @@ -5,6 +5,7 @@ import { useIntl } from 'react-intl'; import { Dialog } from '@onekeyhq/components'; import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import type { EOAuthSocialLoginProvider } from '@onekeyhq/shared/src/consts/authConsts'; import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; import { ETranslations } from '@onekeyhq/shared/src/locale'; @@ -19,6 +20,21 @@ import { useSupabaseAuthContext } from './SupabaseAuthContext'; import type { AuthResponse, SupabaseClient } from '@supabase/supabase-js'; +export type IOAuthSignInResult = { + success: boolean; + session?: { + accessToken: string; + refreshToken: string; + }; +}; + +export type IOAuthSignInOptions = { + // Whether to persist the session to storage and set it in Supabase client + // When false (default): Only return tokens in memory, don't call setSession + // When true: Call setSession to persist and enable auto-refresh + persistSession?: boolean; +}; + export function useSupabaseAuth() { const ctx = useSupabaseAuthContext(); const supabaseUser = ctx?.session?.user; @@ -32,20 +48,9 @@ export function useSupabaseAuth() { const performOAuthSignIn = useCallback( async ( - provider: 'google' | 'apple', - options?: { - // Whether to persist the session to storage and set it in Supabase client - // When false (default): Only return tokens in memory, don't call setSession - // When true: Call setSession to persist and enable auto-refresh - persistSession?: boolean; - }, - ): Promise<{ - success: boolean; - session?: { - accessToken: string; - refreshToken: string; - }; - }> => { + provider: EOAuthSocialLoginProvider, + options?: IOAuthSignInOptions, + ): Promise => { const { persistSession } = options ?? {}; const clientTemp: SupabaseClient = createTemporarySupabaseClient(); @@ -126,14 +131,14 @@ export function useSupabaseAuth() { http://127.0.0.1:62416/oauth_callback_desktop?code=xxxx&onekey_oauth_state=2fd6480e3004ad6aef7d6a72dc37455b */ - Dialog.debugMessage({ - title: 'performOAuthSignIn', - debugMessage: { - provider, - redirectTo, - authUrl, - }, - }); + // Dialog.debugMessage({ + // title: 'performOAuthSignIn', + // debugMessage: { + // provider, + // redirectTo, + // authUrl, + // }, + // }); // Open OAuth popup using platform-specific implementation return OAuthPopup.open({ @@ -147,37 +152,12 @@ export function useSupabaseAuth() { [], ); - const signInWithGoogle = useCallback( - async (options?: { - // Whether to persist the session to storage (default: false) - persistSession?: boolean; - }): Promise<{ - success: boolean; - session?: { - accessToken: string; - refreshToken: string; - }; - }> => { - // Perform the OAuth flow - const oauthResult = await performOAuthSignIn('google', options); - return oauthResult; - }, - [performOAuthSignIn], - ); - - const signInWithApple = useCallback( - async (options?: { - // Whether to persist the session to storage (default: false) - persistSession?: boolean; - }): Promise<{ - success: boolean; - session?: { - accessToken: string; - refreshToken: string; - }; - }> => { - // Perform the OAuth flow - const oauthResult = await performOAuthSignIn('apple', options); + const signInWithSocialLogin = useCallback( + async ( + provider: EOAuthSocialLoginProvider, + options?: IOAuthSignInOptions, + ): Promise => { + const oauthResult = await performOAuthSignIn(provider, options); return oauthResult; }, [performOAuthSignIn], @@ -367,8 +347,7 @@ export function useSupabaseAuth() { () => ({ signOut, signInWithOtp, - signInWithGoogle, - signInWithApple, + signInWithSocialLogin, performOAuthSignIn, verifyOtp, getSupabaseClient, @@ -383,8 +362,7 @@ export function useSupabaseAuth() { [ signOut, signInWithOtp, - signInWithGoogle, - signInWithApple, + signInWithSocialLogin, performOAuthSignIn, verifyOtp, getAccessToken, diff --git a/packages/kit/src/components/OneKeyAuth/useOneKeyAuth.tsx b/packages/kit/src/components/OneKeyAuth/useOneKeyAuth.tsx index eeaf79d36e10..7c020a4a83ed 100644 --- a/packages/kit/src/components/OneKeyAuth/useOneKeyAuth.tsx +++ b/packages/kit/src/components/OneKeyAuth/useOneKeyAuth.tsx @@ -47,6 +47,7 @@ export function useOneKeyAuthMethods() { const [user] = usePrimePersistAtom(); const { + signInWithSocialLogin, signOut: supabaseSignOut, getAccessToken, isReady, @@ -94,6 +95,7 @@ export function useOneKeyAuthMethods() { supabaseSignInWithOtp, supabaseVerifyOtp, supabaseSignOut, + signInWithSocialLogin, }; }, [ getAccessToken, @@ -105,6 +107,7 @@ export function useOneKeyAuthMethods() { supabaseSignInWithOtp, supabaseVerifyOtp, supabaseSignOut, + signInWithSocialLogin, ]); } diff --git a/packages/kit/src/components/Password/container/PasswordSetupContainer.tsx b/packages/kit/src/components/Password/container/PasswordSetupContainer.tsx index 1badf50fc43e..a2ea73f5d7ee 100644 --- a/packages/kit/src/components/Password/container/PasswordSetupContainer.tsx +++ b/packages/kit/src/components/Password/container/PasswordSetupContainer.tsx @@ -31,6 +31,7 @@ import type { IPasswordSetupForm } from '../components/PasswordSetup'; interface IPasswordSetupProps { onSetupRes: (password: string) => void; + pageMode?: boolean; } interface IBiologyAuthContainerProps { @@ -74,7 +75,10 @@ const BiologyAuthContainer = ({ ) : null; }; -const PasswordSetupContainer = ({ onSetupRes }: IPasswordSetupProps) => { +const PasswordSetupContainer = ({ + onSetupRes, + pageMode, +}: IPasswordSetupProps) => { const intl = useIntl(); const [loading, setLoading] = useState(false); const [{ isSupport }] = usePasswordWebAuthInfoAtom(); @@ -166,6 +170,7 @@ const PasswordSetupContainer = ({ onSetupRes }: IPasswordSetupProps) => { return ( + this.withFinalizeWalletSetupStep.call(set, { + createWalletFn: async () => { + const { wallet, indexedAccount, isOverrideWallet } = + await backgroundApiProxy.serviceKeylessWallet.createKeylessWalletV2( + { + token, + }, + ); + await this.autoSelectToCreatedWallet.call(set, { + wallet, + indexedAccount, + isOverrideWallet, + }); + return { wallet, indexedAccount, isOverrideWallet }; + }, + generatingAccountsFn: async ({ wallet, indexedAccount }) => { + await this.addDefaultNetworkAccounts.call(set, { + wallet, + indexedAccount, + }); + }, + }), + ); + createHWWallet = contextAtomMethod( async ( _, @@ -2331,6 +2365,7 @@ export function useAccountSelectorActions() { const createQrWallet = actions.createQrWallet.use(); const createTonImportedWallet = actions.createTonImportedWallet.use(); const createKeylessWallet = actions.createKeylessWallet.use(); + const createKeylessWalletV2 = actions.createKeylessWalletV2.use(); const autoSelectNextAccount = actions.autoSelectNextAccount.use(); const updateHwWalletsDeprecatedStatus = actions.updateHwWalletsDeprecatedStatus.use(); @@ -2370,6 +2405,7 @@ export function useAccountSelectorActions() { createQrWallet, createTonImportedWallet, createKeylessWallet, + createKeylessWalletV2, updateHwWalletsDeprecatedStatus, autoSelectNextAccount, autoSelectNetworkOfOthersWalletAccount, diff --git a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx index 60dbaaed30d9..07e7d15d0676 100644 --- a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx +++ b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx @@ -18,6 +18,7 @@ import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/background import { useSupabaseAuth } from '@onekeyhq/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth'; import { useOneKeyAuth } from '@onekeyhq/kit/src/components/OneKeyAuth/useOneKeyAuth'; import { + EOAuthSocialLoginProvider, SUPABASE_PROJECT_URL, SUPABASE_PUBLIC_API_KEY, } from '@onekeyhq/shared/src/consts/authConsts'; @@ -68,8 +69,6 @@ function OneKeyIDApiTests() { > | null>(null); const { - signInWithGoogle, - signInWithApple, signInWithOtp, verifyOtp, getSession, @@ -79,7 +78,7 @@ function OneKeyIDApiTests() { isReady, } = useSupabaseAuth(); - const { logout } = useOneKeyAuth(); + const { logout, signInWithSocialLogin } = useOneKeyAuth(); const onTryCloseWindow = async () => { if (platformEnv.isNative) { @@ -176,7 +175,10 @@ function OneKeyIDApiTests() { onPress={async () => { try { setLoading('google'); - const result = await signInWithGoogle({ persistSession }); + const result = await signInWithSocialLogin( + EOAuthSocialLoginProvider.Google, + { persistSession }, + ); if (result.success && result.session?.accessToken) { // Set access token setAccessToken(result.session.accessToken); @@ -190,9 +192,9 @@ function OneKeyIDApiTests() { message: 'You are now signed in with Google', }); } - demoLog(result, 'signInWithGoogle'); + demoLog(result, 'signInWithSocialLogin(Google)'); } catch (e) { - demoError(e, 'signInWithGoogle'); + demoError(e, 'signInWithSocialLogin(Google)'); } finally { setLoading(null); } @@ -207,7 +209,10 @@ function OneKeyIDApiTests() { onPress={async () => { try { setLoading('apple'); - const result = await signInWithApple({ persistSession }); + const result = await signInWithSocialLogin( + EOAuthSocialLoginProvider.Apple, + { persistSession }, + ); if (result.success && result.session?.accessToken) { // Set access token setAccessToken(result.session.accessToken); @@ -221,9 +226,9 @@ function OneKeyIDApiTests() { message: 'You are now signed in with Apple', }); } - demoLog(result, 'signInWithApple'); + demoLog(result, 'signInWithSocialLogin(Apple)'); } catch (e) { - demoError(e, 'signInWithApple'); + demoError(e, 'signInWithSocialLogin(Apple)'); } finally { setLoading(null); } diff --git a/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx index 5898d473e51d..f52d937774f8 100644 --- a/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx @@ -1,32 +1,46 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; -import { useRoute } from '@react-navigation/core'; - -import type { IOnboardingParamListV2 } from '@onekeyhq/shared/src/routes'; +import { Dialog } from '@onekeyhq/components'; import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; +import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; +import { AccountSelectorProviderMirror } from '../../../components/AccountSelector/AccountSelectorProvider'; +import { useKeylessWallet } from '../../../components/KeylessWallet/useKeylessWallet'; import useAppNavigation from '../../../hooks/useAppNavigation'; import { PinInputLayout } from '../components/PinInputLayout'; -import type { RouteProp } from '@react-navigation/core'; - function ConfirmPinPage() { const navigation = useAppNavigation(); - const route = - useRoute< - RouteProp - >(); - const { pin: originalPin } = route.params; + const { getKeylessOnboardingPin, cacheKeylessOnboardingPin } = + useKeylessWallet(); + // Use state to store the PIN, fetched only once on mount + const [originalPin, setOriginalPin] = useState(undefined); const [confirmPin, setConfirmPin] = useState(''); const [isValid, setIsValid] = useState(false); const [errorMessage, setErrorMessage] = useState(''); + // Fetch PIN only on mount (commit phase), not during render phase + // This ensures getAndDelete is called only once by the mounted instance + useEffect(() => { + const pin = getKeylessOnboardingPin(); + setOriginalPin(pin); + }, [getKeylessOnboardingPin]); + const handlePinChange = useCallback( (filteredText: string) => { setConfirmPin(filteredText); setErrorMessage(''); + if (!originalPin) { + Dialog.show({ + icon: 'ErrorOutline', + tone: 'destructive', + title: 'Original PIN is not found. Please try again.', + }); + return; + } + // Auto-validate when 4 digits entered if (filteredText.length === 4) { if (filteredText === originalPin) { @@ -43,8 +57,10 @@ function ConfirmPinPage() { ); const handleConfirm = useCallback(() => { + setConfirmPin(''); + cacheKeylessOnboardingPin({ pin: originalPin || '' }); navigation.push(EOnboardingPagesV2.CreatePasscode); - }, [navigation]); + }, [cacheKeylessOnboardingPin, navigation, originalPin]); return ( + + + ); +} + +export { ConfirmPinPageWithContext as default }; diff --git a/packages/kit/src/views/Onboardingv2/pages/CreateOrImportWallet.tsx b/packages/kit/src/views/Onboardingv2/pages/CreateOrImportWallet.tsx index 6bf5834f17ef..b56798006b07 100644 --- a/packages/kit/src/views/Onboardingv2/pages/CreateOrImportWallet.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/CreateOrImportWallet.tsx @@ -150,7 +150,7 @@ function CreateOrImportWallet() { const { enableKeylessWallet, enableKeylessWalletLoading, - enableKeylessWalletV2, + checkKeylessWalletExistence, } = useKeylessWallet(); const walletKeys = ['metamask', 'okx', 'rainbow', 'tokenpocket'] as const; @@ -203,8 +203,8 @@ function CreateOrImportWallet() { // fromScene: EKeylessWalletEnableScene.Onboarding, // }); // navigation.push(EOnboardingPagesV2.OneKeyIDLogin); - await enableKeylessWalletV2(); - }, [enableKeylessWalletV2]); + await checkKeylessWalletExistence(); + }, [checkKeylessWalletExistence]); const handleKeylessWalletLegacyClick = useCallback(async () => { await enableKeylessWallet({ diff --git a/packages/kit/src/views/Onboardingv2/pages/CreatePasscodePage.tsx b/packages/kit/src/views/Onboardingv2/pages/CreatePasscodePage.tsx index 053d1ae8d32b..88232f089001 100644 --- a/packages/kit/src/views/Onboardingv2/pages/CreatePasscodePage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/CreatePasscodePage.tsx @@ -1,12 +1,14 @@ -import { useCallback, useState } from 'react'; +import { Suspense, useCallback, useState } from 'react'; import { useIntl } from 'react-intl'; -import { Page, SizableText, YStack } from '@onekeyhq/components'; +import { Page, SizableText, Spinner, YStack } from '@onekeyhq/components'; import { usePasswordModeAtom } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; import { ETranslations } from '@onekeyhq/shared/src/locale'; +import backgroundApiProxy from '../../../background/instance/backgroundApiProxy'; import PasswordSetup from '../../../components/Password/components/PasswordSetup'; +import PasswordSetupContainer from '../../../components/Password/container/PasswordSetupContainer'; import { OnboardingLayout } from '../components/OnboardingLayout'; import type { IPasswordSetupForm } from '../../../components/Password/components/PasswordSetup'; @@ -44,13 +46,14 @@ function CreatePasscodePage() { You will use this to unlock your wallet. - + }> + { + alert(data); + }} + /> + diff --git a/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx index a93d372c4bb8..9c528ffea99f 100644 --- a/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx @@ -5,7 +5,10 @@ import { useRoute } from '@react-navigation/core'; import { SizableText } from '@onekeyhq/components'; import type { IOnboardingParamListV2 } from '@onekeyhq/shared/src/routes'; import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; +import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; +import { AccountSelectorProviderMirror } from '../../../components/AccountSelector/AccountSelectorProvider'; +import { useKeylessWallet } from '../../../components/KeylessWallet/useKeylessWallet'; import useAppNavigation from '../../../hooks/useAppNavigation'; import { PinInputLayout } from '../components/PinInputLayout'; @@ -16,12 +19,17 @@ function CreatePinPage() { const route = useRoute>(); const { isResetPin } = route.params ?? {}; + const { cacheKeylessOnboardingPin } = useKeylessWallet(); const [pin, setPin] = useState(''); const handleContinue = useCallback(() => { - navigation.push(EOnboardingPagesV2.ConfirmPin, { pin }); - }, [navigation, pin]); + if (pin) { + cacheKeylessOnboardingPin({ pin }); + setPin(''); + navigation.push(EOnboardingPagesV2.ConfirmPin); + } + }, [cacheKeylessOnboardingPin, navigation, pin]); return ( + + + ); +} + +export { CreatePinPageWithContext as default }; diff --git a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx index 923a475a16b2..5817f4412ff5 100644 --- a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx @@ -12,10 +12,23 @@ import { Spinner, YStack, } from '@onekeyhq/components'; -import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; +import { EOAuthSocialLoginProvider } from '@onekeyhq/shared/src/consts/authConsts'; +import type { + EOnboardingV2OneKeyIDLoginMode, + IOnboardingParamListV2, +} from '@onekeyhq/shared/src/routes'; +import { + EOnboardingPagesV2, + IOnboardingParamList, +} from '@onekeyhq/shared/src/routes'; +import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; +import { AccountSelectorProviderMirror } from '../../../components/AccountSelector/AccountSelectorProvider'; +import { useKeylessWallet } from '../../../components/KeylessWallet/useKeylessWallet'; import { ListItem } from '../../../components/ListItem'; +import { useOneKeyAuth } from '../../../components/OneKeyAuth/useOneKeyAuth'; import useAppNavigation from '../../../hooks/useAppNavigation'; +import { useAppRoute } from '../../../hooks/useAppRoute'; import { OnboardingLayout } from '../components/OnboardingLayout'; function OptionItem({ @@ -98,15 +111,60 @@ function OptionItem({ function OneKeyIDLoginPage() { const navigation = useAppNavigation(); const [isLoggingIn, setIsLoggingIn] = useState(false); + const route = useAppRoute< + IOnboardingParamListV2, + EOnboardingPagesV2.OneKeyIDLogin + >(); + const mode: EOnboardingV2OneKeyIDLoginMode | undefined = route?.params?.mode; + + const { logout, signInWithSocialLogin } = useOneKeyAuth(); + const { + createOrRestoreKeylessWallet, + cacheKeylessOnboardingToken, + checkKeylessWalletCreatedOnServer, + } = useKeylessWallet(); - const handleGoogleLogin = useCallback(() => { - setIsLoggingIn(true); + const goToInputPinPage = useCallback( + async ({ token }: { token: string }) => { + const { isCreated } = await checkKeylessWalletCreatedOnServer({ token }); + await cacheKeylessOnboardingToken({ token }); + if (isCreated) { + navigation.push(EOnboardingPagesV2.VerifyPin); + } else { + navigation.push(EOnboardingPagesV2.CreatePin); + } + }, + [ + cacheKeylessOnboardingToken, + checkKeylessWalletCreatedOnServer, + navigation, + ], + ); - setTimeout(() => { - setIsLoggingIn(false); - navigation.push(EOnboardingPagesV2.CreatePin); - }, 1000); - }, [navigation]); + const handleSocialLogin = useCallback( + async (provider: EOAuthSocialLoginProvider) => { + try { + setIsLoggingIn(true); + const result = await signInWithSocialLogin(provider); + if (result?.session?.accessToken) { + await goToInputPinPage({ + token: result.session.accessToken, + }); + } + } finally { + setIsLoggingIn(false); + } + }, + [goToInputPinPage, signInWithSocialLogin], + ); + + const handleGoogleLogin = useCallback(async () => { + await handleSocialLogin(EOAuthSocialLoginProvider.Google); + }, [handleSocialLogin]); + + const handleAppleLogin = useCallback(async () => { + await handleSocialLogin(EOAuthSocialLoginProvider.Apple); + }, [handleSocialLogin]); return ( @@ -134,9 +192,7 @@ function OneKeyIDLoginPage() { color: '$iconActive', y: -1, }} - onPress={() => { - // TODO: Handle Apple login - }} + onPress={handleAppleLogin} /> @@ -156,4 +212,17 @@ function OneKeyIDLoginPage() { ); } -export { OneKeyIDLoginPage as default }; +function OneKeyIDLoginPageWithContext() { + return ( + + + + ); +} + +export { OneKeyIDLoginPageWithContext as default }; diff --git a/packages/shared/src/consts/authConsts.ts b/packages/shared/src/consts/authConsts.ts index 62160e1a57f6..a160fb7484fc 100644 --- a/packages/shared/src/consts/authConsts.ts +++ b/packages/shared/src/consts/authConsts.ts @@ -27,6 +27,12 @@ export const OAUTH_CALLBACK_NATIVE_PATH = 'oauth_callback_native'; // OAuth shared constants (OneKeyAuth) // ============================================================================ +// OAuth provider types (used by OneKeyAuth) +export enum EOAuthSocialLoginProvider { + Google = 'google', + Apple = 'apple', +} + // OAuth method enums (used by OneKeyAuth) export enum EDesktopOAuthMethod { // ✅ RECOMMENDED diff --git a/packages/shared/src/keylessWallet/keylessWalletTypes.ts b/packages/shared/src/keylessWallet/keylessWalletTypes.ts index a06a4c5d2edd..126aa8717ab8 100644 --- a/packages/shared/src/keylessWallet/keylessWalletTypes.ts +++ b/packages/shared/src/keylessWallet/keylessWalletTypes.ts @@ -1,5 +1,7 @@ import type { ECloudBackupProviderType } from '@onekeyhq/shared/src/cloudBackup/cloudBackupTypes'; +import type { JWTPayload } from 'jose'; + export type IKeylessWalletShare = string; // base64 string export type IKeylessWalletUserInfo = { @@ -100,3 +102,18 @@ export type IKeylessWalletRestoredData = { cloudKeyPackData: ICloudKeyPackEncryptedData | undefined; packs: IKeylessWalletPacks; }; + +export type IKeylessBackendShare = { + ownerId: string; + encryptedMnemonic: string; + backendShare: string; +}; + +export type ISupabaseJWTPayload = JWTPayload & { + app_metadata: { + provider: string; + }; + user_metadata: { + sub: string; + }; +}; diff --git a/packages/shared/src/routes/onboardingv2.ts b/packages/shared/src/routes/onboardingv2.ts index 561682e03468..1c7b363aed0c 100644 --- a/packages/shared/src/routes/onboardingv2.ts +++ b/packages/shared/src/routes/onboardingv2.ts @@ -19,6 +19,12 @@ export enum EOnboardingV2KeylessWalletCreationMode { View = 'View', } +export enum EOnboardingV2OneKeyIDLoginMode { + CreateKeylessWallet = 'CreateKeylessWallet', + ImportKeylessWallet = 'ImportKeylessWallet', + VerifyKeylessWallet = 'VerifyKeylessWallet', +} + export enum EOnboardingPagesV2 { GetStarted = 'GetStarted', AddExistingWallet = 'AddExistingWallet', @@ -44,10 +50,10 @@ export enum EOnboardingPagesV2 { OneKeyIDLogin = 'OneKeyIDLogin', CreatePin = 'CreatePin', ConfirmPin = 'ConfirmPin', - CreatePasscode = 'CreatePasscode', VerifyPin = 'VerifyPin', ResetPin = 'ResetPin', NewPinCreated = 'NewPinCreated', + CreatePasscode = 'CreatePasscode', MoreAction = 'MoreAction', } interface IVerifyRecoveryPhraseParams { @@ -119,13 +125,13 @@ export type IOnboardingParamListV2 = { email?: string; mode?: EOnboardingV2KeylessWalletCreationMode; }; - [EOnboardingPagesV2.OneKeyIDLogin]: undefined; + [EOnboardingPagesV2.OneKeyIDLogin]: { + mode: EOnboardingV2OneKeyIDLoginMode; + }; [EOnboardingPagesV2.CreatePin]: { isResetPin?: boolean; }; - [EOnboardingPagesV2.ConfirmPin]: { - pin: string; - }; + [EOnboardingPagesV2.ConfirmPin]: undefined; [EOnboardingPagesV2.CreatePasscode]: undefined; [EOnboardingPagesV2.VerifyPin]: { /** From 86245038a6160ea5d2df85c52740ed42c391cd92 Mon Sep 17 00:00:00 2001 From: morizon Date: Fri, 26 Dec 2025 21:47:22 +0800 Subject: [PATCH 28/66] fix: lint --- .../pages/HardwareTroubleshootingModal/index.tsx | 2 ++ packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx | 2 +- packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/views/DeviceManagement/pages/HardwareTroubleshootingModal/index.tsx b/packages/kit/src/views/DeviceManagement/pages/HardwareTroubleshootingModal/index.tsx index b4e3f662c2b4..66d4e7dcda59 100644 --- a/packages/kit/src/views/DeviceManagement/pages/HardwareTroubleshootingModal/index.tsx +++ b/packages/kit/src/views/DeviceManagement/pages/HardwareTroubleshootingModal/index.tsx @@ -115,6 +115,8 @@ function HardwareTroubleshootingModal() { }, { title: intl.formatMessage({ + // TODO @franco missing translation? + // @ts-ignore id: ETranslations.global_faqs_reset_wallet, }), icon: 'RepeatOutline', diff --git a/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx index b1d33f491797..26251f65370b 100644 --- a/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx @@ -1,9 +1,9 @@ import { useCallback, useEffect, useState } from 'react'; -import { Dialog } from '@onekeyhq/components'; import { useRoute } from '@react-navigation/core'; import { useIntl } from 'react-intl'; +import { Dialog } from '@onekeyhq/components'; import { ETranslations } from '@onekeyhq/shared/src/locale/enum/translations'; import type { IOnboardingParamListV2 } from '@onekeyhq/shared/src/routes'; import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; diff --git a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx index 1b9f5081eca2..085e00fd97b1 100644 --- a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx @@ -14,6 +14,7 @@ import { YStack, } from '@onekeyhq/components'; import { EOAuthSocialLoginProvider } from '@onekeyhq/shared/src/consts/authConsts'; +import { ETranslations } from '@onekeyhq/shared/src/locale/enum/translations'; import type { EOnboardingV2OneKeyIDLoginMode, IOnboardingParamListV2, @@ -23,7 +24,6 @@ import { IOnboardingParamList, } from '@onekeyhq/shared/src/routes'; import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; -import { ETranslations } from '@onekeyhq/shared/src/locale/enum/translations'; import { AccountSelectorProviderMirror } from '../../../components/AccountSelector/AccountSelectorProvider'; import { useKeylessWallet } from '../../../components/KeylessWallet/useKeylessWallet'; From fd75f09369375cbfb243858323f7e5bf35bc3ce0 Mon Sep 17 00:00:00 2001 From: Franco Date: Sat, 27 Dec 2025 10:47:10 +0800 Subject: [PATCH 29/66] responsive design --- .../components/PinInputLayout.tsx | 136 ++++++++++++------ .../Onboardingv2/pages/OneKeyIDLoginPage.tsx | 1 + .../views/Onboardingv2/pages/ResetPinPage.tsx | 29 ++-- 3 files changed, 111 insertions(+), 55 deletions(-) diff --git a/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx b/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx index 06919c7390df..de77c3f53f45 100644 --- a/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx +++ b/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx @@ -1,6 +1,7 @@ import { useCallback, useRef } from 'react'; import { useFocusEffect } from '@react-navigation/core'; +import { KeyboardAvoidingView, type TextInput } from 'react-native'; import { Button, @@ -10,12 +11,12 @@ import { SizableText, XStack, YStack, + useMedia, } from '@onekeyhq/components'; +import platformEnv from '@onekeyhq/shared/src/platformEnv'; import { OnboardingLayout } from './OnboardingLayout'; -import type { TextInput } from 'react-native'; - interface IPinInputLayoutProps { title: string; description?: string | React.ReactNode; @@ -46,12 +47,16 @@ function PinInputLayout({ errorMessage, }: IPinInputLayoutProps) { const inputRef = useRef(null); + const { gtMd } = useMedia(); useFocusEffect( useCallback(() => { - const timer = setTimeout(() => { - inputRef.current?.focus(); - }, 300); + const timer = setTimeout( + () => { + inputRef.current?.focus(); + }, + platformEnv.isNative ? 600 : 300, + ); return () => clearTimeout(timer); }, []), ); @@ -69,45 +74,45 @@ function PinInputLayout({ } }, [isSubmitDisabled, onSubmit]); - return ( - - - - - - - {title} - - {description} - - + const content = ( + + + + + + {title} + + {description} + + - - - - - {errorMessage ? ( - - {errorMessage} - - ) : null} - - + + + + + {errorMessage ? ( + + {errorMessage} + + ) : null} + + + {gtMd ? ( {secondaryButtonText && onSecondaryButtonPress ? ( + {secondaryButtonText && onSecondaryButtonPress ? ( + + ) : null} + + + ) : null} + + ); + + return ( + + {platformEnv.isNative ? ( + + {content} + + ) : ( + content + )} ); } diff --git a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx index b92cd4ed0f1a..7499803166ae 100644 --- a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx @@ -154,6 +154,7 @@ function OneKeyIDLoginPage() { target="_blank" size="$bodySm" color="$textSubdued" + textAlign="center" > TODO — Link to "How to create wallet with Apple or Google account" diff --git a/packages/kit/src/views/Onboardingv2/pages/ResetPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/ResetPinPage.tsx index 9cf2dd4207b2..d472415f7ee8 100644 --- a/packages/kit/src/views/Onboardingv2/pages/ResetPinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/ResetPinPage.tsx @@ -6,6 +6,7 @@ import { SizableText, XStack, YStack, + useMedia, } from '@onekeyhq/components'; import useAppNavigation from '../../../hooks/useAppNavigation'; @@ -30,7 +31,7 @@ const STEPS = [ function ResetPinPage() { const navigation = useAppNavigation(); - + const { gtMd } = useMedia(); const handleDone = useCallback(() => { navigation.pop(); }, [navigation]); @@ -65,22 +66,34 @@ function ResetPinPage() { - - {step.title} - - + {step.title} + {step.description} ))} - + {gtMd ? ( + + ) : null} + {!gtMd ? ( + + + + ) : null} ); From 81174cf1fcad60ebf85fe509720227c9089990fc Mon Sep 17 00:00:00 2001 From: Franco Date: Sat, 27 Dec 2025 17:46:28 +0800 Subject: [PATCH 30/66] i18n & responsive design --- .../components/src/content/Keyboard/index.tsx | 2 +- .../components/PinInputLayout.tsx | 177 +++++++++--------- .../views/Onboardingv2/pages/ResetPinPage.tsx | 66 ++++--- .../Onboardingv2/pages/VerifyPinPage.tsx | 1 + .../shared/src/locale/enum/translations.ts | 10 + packages/shared/src/locale/json/bn.json | 10 + packages/shared/src/locale/json/de.json | 10 + packages/shared/src/locale/json/en_US.json | 10 + packages/shared/src/locale/json/es.json | 10 + packages/shared/src/locale/json/fr_FR.json | 10 + packages/shared/src/locale/json/hi_IN.json | 10 + packages/shared/src/locale/json/id.json | 10 + packages/shared/src/locale/json/it_IT.json | 10 + packages/shared/src/locale/json/ja_JP.json | 10 + packages/shared/src/locale/json/ko_KR.json | 10 + packages/shared/src/locale/json/pt.json | 10 + packages/shared/src/locale/json/pt_BR.json | 10 + packages/shared/src/locale/json/ru.json | 10 + packages/shared/src/locale/json/th_TH.json | 10 + packages/shared/src/locale/json/uk_UA.json | 10 + packages/shared/src/locale/json/vi.json | 10 + packages/shared/src/locale/json/zh_CN.json | 10 + packages/shared/src/locale/json/zh_HK.json | 10 + packages/shared/src/locale/json/zh_TW.json | 10 + 24 files changed, 332 insertions(+), 114 deletions(-) diff --git a/packages/components/src/content/Keyboard/index.tsx b/packages/components/src/content/Keyboard/index.tsx index acd307cd2806..6527f9959aa9 100644 --- a/packages/components/src/content/Keyboard/index.tsx +++ b/packages/components/src/content/Keyboard/index.tsx @@ -3,7 +3,7 @@ import { dismissKeyboardWithDelay, } from '@onekeyhq/shared/src/keyboard'; -const PassThrough = (children: React.ReactNode) => children; +const PassThrough = ({ children }: { children?: React.ReactNode }) => children; export const Keyboard = { AvoidingView: PassThrough, diff --git a/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx b/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx index de77c3f53f45..a81462d74a8a 100644 --- a/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx +++ b/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx @@ -1,12 +1,13 @@ -import { useCallback, useRef } from 'react'; +import { useCallback, useMemo, useRef } from 'react'; import { useFocusEffect } from '@react-navigation/core'; -import { KeyboardAvoidingView, type TextInput } from 'react-native'; +import { type TextInput } from 'react-native'; import { Button, HeightTransition, Input, + Keyboard, Page, SizableText, XStack, @@ -30,6 +31,7 @@ interface IPinInputLayoutProps { isSubmitDisabled?: boolean; isInputDisabled?: boolean; errorMessage?: string; + placeholder?: string; } function PinInputLayout({ @@ -45,6 +47,7 @@ function PinInputLayout({ isSubmitDisabled = false, isInputDisabled = false, errorMessage, + placeholder = '••••', }: IPinInputLayoutProps) { const inputRef = useRef(null); const { gtMd } = useMedia(); @@ -55,7 +58,7 @@ function PinInputLayout({ () => { inputRef.current?.focus(); }, - platformEnv.isNative ? 600 : 300, + platformEnv.isNative ? 500 : 300, ); return () => clearTimeout(timer); }, []), @@ -74,109 +77,101 @@ function PinInputLayout({ } }, [isSubmitDisabled, onSubmit]); - const content = ( - - - - - - {title} - - {description} - - + return ( + + + + + + + {title} + + {description} + + - - - - - {errorMessage ? ( - - {errorMessage} - - ) : null} - - - {gtMd ? ( - - {secondaryButtonText && onSecondaryButtonPress ? ( + + + + + {errorMessage ? ( + + {errorMessage} + + ) : null} + + + {gtMd ? ( + + {secondaryButtonText && onSecondaryButtonPress ? ( + + ) : null} - ) : null} + + ) : null} + + + + {!gtMd ? ( + + + - - ) : null} - - - - {!gtMd ? ( - - - - {secondaryButtonText && onSecondaryButtonPress ? ( - - ) : null} - - - ) : null} - - ); - - return ( - - {platformEnv.isNative ? ( - - {content} - - ) : ( - content - )} + {secondaryButtonText && onSecondaryButtonPress ? ( + + ) : null} + + + + ) : null} + ); } diff --git a/packages/kit/src/views/Onboardingv2/pages/ResetPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/ResetPinPage.tsx index d472415f7ee8..1422faafa5f6 100644 --- a/packages/kit/src/views/Onboardingv2/pages/ResetPinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/ResetPinPage.tsx @@ -1,5 +1,7 @@ import { useCallback } from 'react'; +import { useIntl } from 'react-intl'; + import { Button, Page, @@ -8,33 +10,46 @@ import { YStack, useMedia, } from '@onekeyhq/components'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; import useAppNavigation from '../../../hooks/useAppNavigation'; import { OnboardingLayout } from '../components/OnboardingLayout'; -const STEPS = [ - { - title: 'Open Other Device', - description: - 'Go to another device where your OneKey account is signed in with your email', - }, - { - title: 'Go to Settings', - description: 'In Settings, select "Security" and then "Reset PIN"', - }, - { - title: 'Set Your New PIN', - description: - "Once you've set your new PIN, you can now login to your wallet on this device", - }, -]; - function ResetPinPage() { + const intl = useIntl(); const navigation = useAppNavigation(); const { gtMd } = useMedia(); const handleDone = useCallback(() => { navigation.pop(); }, [navigation]); + + const STEPS = [ + { + title: intl.formatMessage({ + id: ETranslations.reset_pin_open_other_device, + }), + description: intl.formatMessage({ + id: ETranslations.reset_pin_open_other_device_desc, + }), + }, + { + title: intl.formatMessage({ + id: ETranslations.reset_pin_go_to_settings, + }), + description: intl.formatMessage({ + id: ETranslations.reset_pin_go_to_settings_desc, + }), + }, + { + title: intl.formatMessage({ + id: ETranslations.reset_pin_set_your_new_pin, + }), + description: intl.formatMessage({ + id: ETranslations.reset_pin_set_your_new_pin_desc, + }), + }, + ]; + return ( @@ -43,11 +58,14 @@ function ResetPinPage() { - Reset PIN using another device + {intl.formatMessage({ + id: ETranslations.reset_pin_using_another_device, + })} - For security, you can only reset your PIN in other devices where - you are logged in + {intl.formatMessage({ + id: ETranslations.reset_pin_using_another_device_desc, + })} @@ -76,7 +94,9 @@ function ResetPinPage() { {gtMd ? ( ) : null} @@ -90,7 +110,9 @@ function ResetPinPage() { variant="primary" onPress={handleDone} > - I've done these steps + {intl.formatMessage({ + id: ETranslations.i_have_done_these_steps, + })} ) : null} diff --git a/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx index 8a81269a666e..f696bc7f5fe0 100644 --- a/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx @@ -192,6 +192,7 @@ function VerifyPinPage() { return ( swap_actionSwap অথবা bridge_actionBridge.", "promode.swap_unsupported_title": "সোয়াপ অসমর্থিত", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "আমি পুনরুদ্ধার বাক্যাংশটি লিখে রেখেছি", "reset_app_desc": "এটি OneKey এ আপনার তৈরি সমস্ত ডেটা মুছে ফেলবে। আপনার যথাযথ ব্যাকআপ রয়েছে তা নিশ্চিত হওয়ার পরে, অ্যাপটি রিসেট করতে \"RESET\" লিখুন", "reset_pin": "পিন রিসেট করুন", + "reset_pin_go_to_settings": "সেটিংসে যান", + "reset_pin_go_to_settings_desc": "Settings-এ \"Security\" নির্বাচন করুন এবং তারপর \"Reset PIN\" নির্বাচন করুন", + "reset_pin_open_other_device": "অন্য ডিভাইস খুলুন", + "reset_pin_open_other_device_desc": "অন্য একটি ডিভাইসে যান যেখানে আপনার সামাজিক অ্যাকাউন্টে সাইন ইন করা আছে", + "reset_pin_set_your_new_pin": "আপনার নতুন পিন সেট করুন", + "reset_pin_set_your_new_pin_desc": "আপনি একবার নতুন পিন সেট করলে, এখন এই ডিভাইসে আপনার ওয়ালেটে লগ ইন করতে পারবেন", + "reset_pin_using_another_device": "অন্য ডিভাইস ব্যবহার করে পিন রিসেট করুন", + "reset_pin_using_another_device_desc": "নিরাপত্তার স্বার্থে, আপনি কেবল সেই অন্যান্য ডিভাইসগুলোতেই আপনার পিন রিসেট করতে পারবেন যেখানে আপনি লগ ইন আছেন", "scan.camera_access_denied": "ক্যামেরা অ্যাক্সেস অস্বীকার করা হয়েছে", "scan.enable_camera_permissions": "OneKey কিউআর কোড স্ক্যান করতে ক্যামেরা অ্যাক্সেসের প্রয়োজন। দয়া করে \"সেটিংস\" এ যান এবং এই বৈশিষ্ট্যটি ব্যবহার করতে ক্যামেরা অনুমতিগুলি সক্রিয় করুন।", "scan.grant_camera_access_in_expand_view": "দয়া করে এক্সপ্যান্ড ভিউতে ক্যামেরা অ্যাক্সেস অনুমোদন করুন।", diff --git a/packages/shared/src/locale/json/de.json b/packages/shared/src/locale/json/de.json index 98a54595418e..5d97590a78bd 100644 --- a/packages/shared/src/locale/json/de.json +++ b/packages/shared/src/locale/json/de.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "Primärkonto auf {account} umstellen?", "homescreen.format_supported": "Das Format { token } wird nicht unterstützt.", "hw_banner_description": "Sichern Sie Ihre Krypto mit der leistungsstärksten Hardware-Wallet", + "i_have_done_these_steps": "Ich habe diese Schritte durchgeführt", "id.delete_double_check": "Möchten Sie OneKey ID wirklich löschen?", "id.delete_onekey_id": "OneKey-ID löschen", "id.delete_onekey_id_desc": "Wenn Sie Ihre OneKey-ID löschen, verlieren Sie den Zugriff auf folgende Vorteile. Diese Aktion kann nicht rückgängig gemacht werden.", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "Dein Private-Key-Konto ist bereit", "private_key_imported_feedback_title": "Erfolgreich importiert", "private_key_staty_on_device": "Private Schlüssel bleiben auf dem Gerät", + "promode.limit_sell_for": "Verkaufen für", "promode.swap_unsupported_message": "Dieses Token wird nicht unterstützt, versuchen Sie swap_actionSwap oder bridge_actionBridge.", "promode.swap_unsupported_title": "Swap nicht unterstützt", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "Ich habe den Wiederherstellungscode aufgeschrieben", "reset_app_desc": "Dies wird alle Daten löschen, die Sie auf OneKey erstellt haben. Stellen Sie sicher, dass Sie ein ordnungsgemäßes Backup haben, geben Sie \"RESET\" ein, um die App zurückzusetzen", "reset_pin": "PIN zurücksetzen", + "reset_pin_go_to_settings": "Zu den Einstellungen wechseln", + "reset_pin_go_to_settings_desc": "Wähle in den Einstellungen „Sicherheit“ und dann „PIN zurücksetzen“ aus", + "reset_pin_open_other_device": "Anderes Gerät öffnen", + "reset_pin_open_other_device_desc": "Wechsle zu einem anderen Gerät, auf dem du mit deinem Social-Media-Konto angemeldet bist", + "reset_pin_set_your_new_pin": "Legen Sie Ihre neue PIN fest", + "reset_pin_set_your_new_pin_desc": "Sobald Sie Ihre neue PIN festgelegt haben, können Sie sich jetzt mit diesem Gerät in Ihre Wallet einloggen.", + "reset_pin_using_another_device": "PIN mit einem anderen Gerät zurücksetzen", + "reset_pin_using_another_device_desc": "Aus Sicherheitsgründen kannst du deine PIN nur auf anderen Geräten zurücksetzen, auf denen du angemeldet bist.", "scan.camera_access_denied": "Zugriff auf die Kamera verweigert", "scan.enable_camera_permissions": "OneKey benötigt Kamerazugriff, um QR-Codes zu scannen. Bitte gehen Sie zu den „Einstellungen“ und aktivieren Sie die Kameraberechtigungen, um diese Funktion zu nutzen.", "scan.grant_camera_access_in_expand_view": "Bitte gewähren Sie Kamerazugriff in der erweiterten Ansicht.", diff --git a/packages/shared/src/locale/json/en_US.json b/packages/shared/src/locale/json/en_US.json index 314e87f41223..65a2d4f82fdd 100644 --- a/packages/shared/src/locale/json/en_US.json +++ b/packages/shared/src/locale/json/en_US.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "Switch primary account to {account}?", "homescreen.format_supported": "The { token } format is not supported.", "hw_banner_description": "Secure your crypto with the most powerful hardware wallet", + "i_have_done_these_steps": "I've done these steps", "id.delete_double_check": "Are you sure you want to delete OneKey ID ?", "id.delete_onekey_id": "Delete OneKey ID", "id.delete_onekey_id_desc": "By deleting OneKey ID, you will lose access to the following benefits. This action cannot be undone.", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "Your private key account is ready", "private_key_imported_feedback_title": "Imported successfully", "private_key_staty_on_device": "Private keys stay on device", + "promode.limit_sell_for": "Sell for", "promode.swap_unsupported_message": "This token is not supported, Try swap_actionSwap or bridge_actionBridge.", "promode.swap_unsupported_title": "Swap unsupported", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "I've written down the recovery phrase", "reset_app_desc": "This will delete all the data you have created on OneKey. After making sure that you have a proper backup, enter \"RESET\" to reset the App", "reset_pin": "Reset PIN", + "reset_pin_go_to_settings": "Go to Settings", + "reset_pin_go_to_settings_desc": "In Settings, select \"Security\" and then \"Reset PIN\"", + "reset_pin_open_other_device": "Open other device", + "reset_pin_open_other_device_desc": "Go to another device where your social account is signed in", + "reset_pin_set_your_new_pin": "Set Your New PIN", + "reset_pin_set_your_new_pin_desc": "Once you've set your new PIN, you can now log in to your wallet on this device", + "reset_pin_using_another_device": "Reset PIN using another device", + "reset_pin_using_another_device_desc": "For security, you can only reset your PIN in other devices where you are logged in", "scan.camera_access_denied": "Camera access denied", "scan.enable_camera_permissions": "OneKey requires camera access to scan QR codes. Please go to “Settings” and enable camera permissions to use this feature.", "scan.grant_camera_access_in_expand_view": "Please grant camera access in the expand view.", diff --git a/packages/shared/src/locale/json/es.json b/packages/shared/src/locale/json/es.json index d735b43487dc..05f252b7d188 100644 --- a/packages/shared/src/locale/json/es.json +++ b/packages/shared/src/locale/json/es.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "¿Cambiar la cuenta principal a {account}?", "homescreen.format_supported": "El formato { token } no es compatible.", "hw_banner_description": "Asegura tu cripto con la cartera de hardware más potente", + "i_have_done_these_steps": "He hecho estos pasos", "id.delete_double_check": "¿Realmente quieres eliminar OneKey ID?", "id.delete_onekey_id": "Eliminar ID de OneKey", "id.delete_onekey_id_desc": "Al eliminar tu ID de OneKey, perderás el acceso a los siguientes beneficios. Esta acción es irreversible.", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "Tu cuenta de clave privada está lista", "private_key_imported_feedback_title": "Importado correctamente", "private_key_staty_on_device": "Las claves privadas permanecen en el dispositivo", + "promode.limit_sell_for": "Vender por", "promode.swap_unsupported_message": "This token is not supported, Try swap_actionSwap or bridge_actionBridge.", "promode.swap_unsupported_title": "Intercambio no compatible", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "He escrito la frase de recuperación", "reset_app_desc": "Esto eliminará todos los datos que hayas creado en OneKey. Después de asegurarte de que tienes una copia de seguridad adecuada, ingresa \"RESET\" para reiniciar la App", "reset_pin": "Restablecer PIN", + "reset_pin_go_to_settings": "Ir a Configuración", + "reset_pin_go_to_settings_desc": "En Configuración, selecciona \"Seguridad\" y luego \"Restablecer PIN\"", + "reset_pin_open_other_device": "Abrir otro dispositivo", + "reset_pin_open_other_device_desc": "Ve a otro dispositivo donde tu cuenta social esté iniciada", + "reset_pin_set_your_new_pin": "Configura tu nuevo PIN", + "reset_pin_set_your_new_pin_desc": "Una vez que hayas configurado tu nuevo PIN, ya podrás iniciar sesión en tu billetera en este dispositivo", + "reset_pin_using_another_device": "Restablecer PIN usando otro dispositivo", + "reset_pin_using_another_device_desc": "Por seguridad, solo puedes restablecer tu PIN en otros dispositivos donde hayas iniciado sesión", "scan.camera_access_denied": "Acceso a la cámara denegado", "scan.enable_camera_permissions": "OneKey requiere acceso a la cámara para escanear códigos QR. Por favor, vaya a \"Configuración\" y habilite los permisos de la cámara para usar esta función.", "scan.grant_camera_access_in_expand_view": "Por favor, concede acceso a la cámara en la vista expandida.", diff --git a/packages/shared/src/locale/json/fr_FR.json b/packages/shared/src/locale/json/fr_FR.json index 267f8b0cf626..7de351865437 100644 --- a/packages/shared/src/locale/json/fr_FR.json +++ b/packages/shared/src/locale/json/fr_FR.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "Passer au compte principal {account} ?", "homescreen.format_supported": "Le format { token } n'est pas pris en charge.", "hw_banner_description": "Sécurisez votre crypto avec le portefeuille matériel le plus puissant", + "i_have_done_these_steps": "J'ai effectué ces étapes", "id.delete_double_check": "Voulez-vous vraiment supprimer OneKey ID ?", "id.delete_onekey_id": "Supprimer l'identifiant OneKey", "id.delete_onekey_id_desc": "En supprimant votre identifiant OneKey, vous perdrez l'accès aux avantages suivants. Cette action est irréversible.", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "Votre compte de clé privée est prêt", "private_key_imported_feedback_title": "Importé avec succès", "private_key_staty_on_device": "Les clés privées restent sur l'appareil", + "promode.limit_sell_for": "Vendre pour", "promode.swap_unsupported_message": "Ce jeton n'est pas pris en charge. Essayez swap_actionÉchanger ou bridge_actionPont.", "promode.swap_unsupported_title": "Échange non pris en charge", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "J'ai noté la phrase de récupération", "reset_app_desc": "Cela supprimera toutes les données que vous avez créées sur OneKey. Après vous être assuré d'avoir une sauvegarde appropriée, entrez \"RESET\" pour réinitialiser l'application", "reset_pin": "Réinitialiser le code PIN", + "reset_pin_go_to_settings": "Aller dans Paramètres", + "reset_pin_go_to_settings_desc": "Dans Paramètres, sélectionnez « Sécurité » puis « Réinitialiser le code PIN »", + "reset_pin_open_other_device": "Ouvrir un autre appareil", + "reset_pin_open_other_device_desc": "Accédez à un autre appareil sur lequel votre compte social est connecté", + "reset_pin_set_your_new_pin": "Définir votre nouveau code PIN", + "reset_pin_set_your_new_pin_desc": "Une fois que vous avez défini votre nouveau code PIN, vous pouvez maintenant vous connecter à votre portefeuille sur cet appareil", + "reset_pin_using_another_device": "Réinitialiser le code PIN à l'aide d'un autre appareil", + "reset_pin_using_another_device_desc": "Pour des raisons de sécurité, vous ne pouvez réinitialiser votre code PIN que sur les autres appareils où vous êtes connecté", "scan.camera_access_denied": "Accès à la caméra refusé", "scan.enable_camera_permissions": "OneKey nécessite l'accès à la caméra pour scanner les codes QR. Veuillez aller dans \"Paramètres\" et activer les autorisations de la caméra pour utiliser cette fonctionnalité.", "scan.grant_camera_access_in_expand_view": "Veuillez autoriser l'accès à la caméra dans la vue étendue.", diff --git a/packages/shared/src/locale/json/hi_IN.json b/packages/shared/src/locale/json/hi_IN.json index 988efb762afa..44b1db9716cf 100644 --- a/packages/shared/src/locale/json/hi_IN.json +++ b/packages/shared/src/locale/json/hi_IN.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "प्राथमिक खाता {account} में बदलें?", "homescreen.format_supported": "{ token } प्रारूप समर्थित नहीं है.", "hw_banner_description": "अपने क्रिप्टो को सबसे शक्तिशाली हार्डवेयर वॉलेट के साथ सुरक्षित करें", + "i_have_done_these_steps": "मैंने ये कदम पूरे कर लिए हैं", "id.delete_double_check": "क्या आप सच में OneKey ID हटाना चाहते हैं?", "id.delete_onekey_id": "OneKey ID को हटाएं", "id.delete_onekey_id_desc": "OneKey ID हटाने पर आप निम्नलिखित लाभों को खो देंगे। यह कार्रवाई वापस नहीं की जा सकती।", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "आपका प्राइवेट की खाता तैयार है", "private_key_imported_feedback_title": "सफलतापूर्वक आयात किया गया", "private_key_staty_on_device": "निजी कुंजियाँ डिवाइस पर रहती हैं", + "promode.limit_sell_for": "इसके लिए बेचें", "promode.swap_unsupported_message": "यह टोकन समर्थित नहीं है, swap_actionस्वैप या bridge_actionब्रिज आज़माएं।", "promode.swap_unsupported_title": "स्वैप असमर्थित", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "मैंने रिकवरी फ़्रेज़ लिख लिया है", "reset_app_desc": "यह OneKey पर आपके द्वारा बनाए गए सभी डेटा को हटा देगा। सुनिश्चित करने के बाद कि आपके पास उचित बैकअप है, ऐप को रीसेट करने के लिए \"RESET\" दर्ज करें।", "reset_pin": "पिन रीसेट करें", + "reset_pin_go_to_settings": "सेटिंग्स पर जाएं", + "reset_pin_go_to_settings_desc": "सेटिंग्स में, \"सुरक्षा\" चुनें और फिर \"पिन रीसेट करें\"", + "reset_pin_open_other_device": "अन्य डिवाइस खोलें", + "reset_pin_open_other_device_desc": "किसी अन्य डिवाइस पर जाएं जहां आपका सोशल अकाउंट साइन इन है", + "reset_pin_set_your_new_pin": "अपना नया पिन सेट करें", + "reset_pin_set_your_new_pin_desc": "एक बार जब आप अपना नया पिन सेट कर लेते हैं, तो अब आप इस डिवाइस पर अपने वॉलेट में लॉग इन कर सकते हैं", + "reset_pin_using_another_device": "किसी अन्य डिवाइस का उपयोग करके PIN रीसेट करें", + "reset_pin_using_another_device_desc": "सुरक्षा के लिए, आप केवल उन्हीं अन्य डिवाइस पर अपना PIN रीसेट कर सकते हैं जहां आप लॉग इन हैं", "scan.camera_access_denied": "कैमरा पहुंच अस्वीकृत", "scan.enable_camera_permissions": "OneKey को QR कोड स्कैन करने के लिए कैमरा की आवश्यकता होती है। कृपया \"सेटिंग्स\" पर जाएं और इस सुविधा का उपयोग करने के लिए कैमरा अनुमतियां सक्षम करें।", "scan.grant_camera_access_in_expand_view": "कृपया विस्तार दृश्य में कैमरा पहुंच प्रदान करें।", diff --git a/packages/shared/src/locale/json/id.json b/packages/shared/src/locale/json/id.json index 224ac1a1180c..3cc7162be74f 100644 --- a/packages/shared/src/locale/json/id.json +++ b/packages/shared/src/locale/json/id.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "Ubah akun utama ke {account}?", "homescreen.format_supported": "Format { token } tidak didukung.", "hw_banner_description": "Amanakan kripto Anda dengan dompet perangkat keras yang paling kuat", + "i_have_done_these_steps": "Saya sudah melakukan langkah-langkah ini", "id.delete_double_check": "Yakin ingin menghapus OneKey ID?", "id.delete_onekey_id": "Hapus ID OneKey", "id.delete_onekey_id_desc": "Dengan menghapus ID OneKey, Anda akan kehilangan akses ke keuntungan berikut. Tindakan ini tidak dapat dibatalkan.", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "Akun kunci pribadi Anda siap", "private_key_imported_feedback_title": "Berhasil diimpor", "private_key_staty_on_device": "Kunci privat tetap di perangkat", + "promode.limit_sell_for": "Jual seharga", "promode.swap_unsupported_message": "Token ini tidak didukung, Coba swap_actionTukar atau bridge_actionJembatan.", "promode.swap_unsupported_title": "Swap tidak didukung", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "Saya telah menulis frase pemulihan", "reset_app_desc": "Ini akan menghapus semua data yang telah Anda buat di OneKey. Setelah memastikan bahwa Anda memiliki cadangan yang tepat, masukkan \"RESET\" untuk mereset Aplikasi", "reset_pin": "Atur Ulang PIN", + "reset_pin_go_to_settings": "Buka Pengaturan", + "reset_pin_go_to_settings_desc": "Di Pengaturan, pilih \"Keamanan\" lalu \"Atur Ulang PIN\"", + "reset_pin_open_other_device": "Buka perangkat lain", + "reset_pin_open_other_device_desc": "Buka perangkat lain tempat akun sosial Anda masuk", + "reset_pin_set_your_new_pin": "Atur PIN Baru Anda", + "reset_pin_set_your_new_pin_desc": "Setelah Anda menetapkan PIN baru, Anda sekarang dapat masuk ke dompet Anda di perangkat ini", + "reset_pin_using_another_device": "Atur Ulang PIN menggunakan perangkat lain", + "reset_pin_using_another_device_desc": "Demi keamanan, Anda hanya dapat mengatur ulang PIN di perangkat lain tempat Anda sudah masuk", "scan.camera_access_denied": "Akses kamera ditolak", "scan.enable_camera_permissions": "OneKey memerlukan akses kamera untuk memindai kode QR. Silakan pergi ke \"Pengaturan\" dan aktifkan izin kamera untuk menggunakan fitur ini.", "scan.grant_camera_access_in_expand_view": "Silakan berikan akses kamera dalam tampilan perluas.", diff --git a/packages/shared/src/locale/json/it_IT.json b/packages/shared/src/locale/json/it_IT.json index 5da17dd2408a..1e46d29fb7e5 100644 --- a/packages/shared/src/locale/json/it_IT.json +++ b/packages/shared/src/locale/json/it_IT.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "Passare all'account principale {account}?", "homescreen.format_supported": "Il formato { token } non è supportato.", "hw_banner_description": "Proteggi le tue criptovalute con il portafoglio hardware più potente", + "i_have_done_these_steps": "Ho completato questi passaggi", "id.delete_double_check": "Vuoi davvero eliminare OneKey ID?", "id.delete_onekey_id": "Elimina ID OneKey", "id.delete_onekey_id_desc": "Eliminando il tuo ID OneKey, perderai l'accesso ai seguenti vantaggi. Questa azione è irreversibile.", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "Il tuo account con chiave privata è pronto", "private_key_imported_feedback_title": "Importazione riuscita", "private_key_staty_on_device": "Le chiavi private rimangono sul dispositivo", + "promode.limit_sell_for": "Vendi a", "promode.swap_unsupported_message": "Questo token non è supportato. Prova swap_actionSwap o bridge_actionBridge.", "promode.swap_unsupported_title": "Swap non supportato", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "Ho annotato la frase di recupero", "reset_app_desc": "Questo cancellerà tutti i dati che hai creato su OneKey. Dopo esserti assicurato di avere un backup adeguato, inserisci \"RESET\" per ripristinare l'App", "reset_pin": "Reimposta PIN", + "reset_pin_go_to_settings": "Vai a Impostazioni", + "reset_pin_go_to_settings_desc": "In Impostazioni, seleziona \"Sicurezza\" e poi \"Reimposta PIN\"", + "reset_pin_open_other_device": "Apri altro dispositivo", + "reset_pin_open_other_device_desc": "Vai su un altro dispositivo in cui hai effettuato l'accesso con il tuo account social", + "reset_pin_set_your_new_pin": "Imposta il Tuo Nuovo PIN", + "reset_pin_set_your_new_pin_desc": "Una volta impostato il nuovo PIN, ora puoi accedere al tuo portafoglio su questo dispositivo", + "reset_pin_using_another_device": "Reimposta il PIN utilizzando un altro dispositivo", + "reset_pin_using_another_device_desc": "Per motivi di sicurezza, puoi reimpostare il tuo PIN solo su altri dispositivi in cui hai effettuato l'accesso", "scan.camera_access_denied": "Accesso alla fotocamera negato", "scan.enable_camera_permissions": "OneKey richiede l'accesso alla fotocamera per scansionare i codici QR. Si prega di andare su \"Impostazioni\" e abilitare i permessi della fotocamera per utilizzare questa funzione.", "scan.grant_camera_access_in_expand_view": "Concedi l'accesso alla fotocamera nella vista espansa.", diff --git a/packages/shared/src/locale/json/ja_JP.json b/packages/shared/src/locale/json/ja_JP.json index bab0412c6f2d..e042970410dc 100644 --- a/packages/shared/src/locale/json/ja_JP.json +++ b/packages/shared/src/locale/json/ja_JP.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "主なアカウントを{account}に切り替えますか?", "homescreen.format_supported": "ザ { token } format はサポートされていません。", "hw_banner_description": "最強力なハードウェアウォレットであなたの暗号を保護します", + "i_have_done_these_steps": "これらの手順を実行しました", "id.delete_double_check": "本当にOneKey IDを削除しますか?", "id.delete_onekey_id": "OneKey IDを削除する", "id.delete_onekey_id_desc": "OneKey IDを削除すると、以下の特典が利用できなくなります。この操作は取り消すことができません。", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "あなたの秘密鍵アカウントの準備ができました", "private_key_imported_feedback_title": "インポートに成功しました", "private_key_staty_on_device": "秘密鍵はデバイス上に保管されます", + "promode.limit_sell_for": "売却価格", "promode.swap_unsupported_message": "このトークンはサポートされていません。swap_actionスワップまたはbridge_actionブリッジをお試しください。", "promode.swap_unsupported_title": "スワップはサポートされていません", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "リカバリーフレーズを書き留めました", "reset_app_desc": "これにより、OneKey上で作成したすべてのデータが削除されます。適切なバックアップを取ったことを確認した後、アプリをリセットするために\"RESET\"と入力してください。", "reset_pin": "PINをリセット", + "reset_pin_go_to_settings": "設定に移動", + "reset_pin_go_to_settings_desc": "設定で「セキュリティ」を選択し、次に「PINをリセット」を選択してください", + "reset_pin_open_other_device": "他のデバイスを開く", + "reset_pin_open_other_device_desc": "ソーシャルアカウントにサインインしている別のデバイスに移動してください", + "reset_pin_set_your_new_pin": "新しいPINを設定", + "reset_pin_set_your_new_pin_desc": "新しいPINを設定すると、このデバイスでウォレットにログインできるようになります", + "reset_pin_using_another_device": "別のデバイスを使用してPINをリセット", + "reset_pin_using_another_device_desc": "セキュリティのため、PINのリセットはログイン済みの他のデバイスでのみ可能です", "scan.camera_access_denied": "カメラへのアクセスが拒否されました", "scan.enable_camera_permissions": "OneKeyはQRコードをスキャンするためにカメラへのアクセスが必要です。この機能を使用するには、「設定」に移動してカメラの権限を有効にしてください。", "scan.grant_camera_access_in_expand_view": "拡大表示でカメラへのアクセスを許可してください。", diff --git a/packages/shared/src/locale/json/ko_KR.json b/packages/shared/src/locale/json/ko_KR.json index 5a7f448e0664..a93a54fa37ef 100644 --- a/packages/shared/src/locale/json/ko_KR.json +++ b/packages/shared/src/locale/json/ko_KR.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "주 계정을 {account}(으)로 전환하시겠습니까?", "homescreen.format_supported": "{ token } 형식은 지원되지 않습니다.", "hw_banner_description": "보안과 자산증식을 위한 가장 강력한 도구", + "i_have_done_these_steps": "이 단계들을 완료했습니다", "id.delete_double_check": "정말 OneKey ID를 삭제하시겠습니까?", "id.delete_onekey_id": "OneKey ID 삭제", "id.delete_onekey_id_desc": "OneKey ID를 삭제하면 다음 혜택을 이용할 수 없게 됩니다. 이 작업은 되돌릴 수 없습니다.", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "개인 키 계정이 준비되었습니다", "private_key_imported_feedback_title": "성공적으로 가져왔습니다", "private_key_staty_on_device": "개인 키는 기기 안에만 저장됩니다", + "promode.limit_sell_for": "판매가", "promode.swap_unsupported_message": "이 토큰은 지원되지 않습니다. swap_action스왑 또는 bridge_action브리지를 사용해 보세요.", "promode.swap_unsupported_title": "스왑 지원 안 됨", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "나는 복구 구문을 적어 놓았습니다", "reset_app_desc": "이 작업은 OneKey에서 생성한 모든 데이터를 삭제합니다. 적절한 백업을 갖추었는지 확인한 후 \"RESET\"을 입력하여 앱을 초기화하세요.", "reset_pin": "PIN 재설정", + "reset_pin_go_to_settings": "설정으로 이동", + "reset_pin_go_to_settings_desc": "설정에서 \"보안\"을 선택한 다음 \"PIN 재설정\"을 선택하세요.", + "reset_pin_open_other_device": "다른 기기 열기", + "reset_pin_open_other_device_desc": "소셜 계정에 로그인되어 있는 다른 기기로 이동하세요", + "reset_pin_set_your_new_pin": "새 PIN 설정", + "reset_pin_set_your_new_pin_desc": "새 PIN을 설정하셨다면 이제 이 기기에서 지갑에 로그인할 수 있습니다.", + "reset_pin_using_another_device": "다른 기기에서 PIN 재설정", + "reset_pin_using_another_device_desc": "보안을 위해, 현재 로그인된 다른 기기에서만 PIN을 재설정할 수 있습니다.", "scan.camera_access_denied": "카메라 접근이 거부되었습니다", "scan.enable_camera_permissions": "OneKey는 QR 코드를 스캔하기 위해 카메라 접근 권한이 필요합니다. 이 기능을 사용하려면 \"설정\"으로 이동하여 카메라 권한을 활성화해 주세요.", "scan.grant_camera_access_in_expand_view": "확장 뷰에서 카메라 접근 권한을 부여해 주세요.", diff --git a/packages/shared/src/locale/json/pt.json b/packages/shared/src/locale/json/pt.json index a844fa0184cb..ebe92a369e93 100644 --- a/packages/shared/src/locale/json/pt.json +++ b/packages/shared/src/locale/json/pt.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "Alterar a conta principal para {account}?", "homescreen.format_supported": "O formato { token } não é suportado.", "hw_banner_description": "Proteja sua criptomoeda com a carteira de hardware mais poderosa", + "i_have_done_these_steps": "Eu fiz esses passos", "id.delete_double_check": "Tem a certeza que deseja eliminar OneKey ID?", "id.delete_onekey_id": "Eliminar ID OneKey", "id.delete_onekey_id_desc": "Ao eliminar o ID OneKey, perderá o acesso aos seguintes benefícios. Esta ação é irreversível.", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "A sua conta de chave privada está pronta", "private_key_imported_feedback_title": "Importado com sucesso", "private_key_staty_on_device": "As chaves privadas permanecem no dispositivo", + "promode.limit_sell_for": "Vender por", "promode.swap_unsupported_message": "Este token não é suportado. Tente swap_actionTrocar ou bridge_actionPonte.", "promode.swap_unsupported_title": "Swap não suportado", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "Eu anotei a frase de recuperação", "reset_app_desc": "Isso irá apagar todos os dados que você criou no OneKey. Após garantir que você tem um backup adequado, digite \"RESET\" para redefinir o App", "reset_pin": "Redefinir PIN", + "reset_pin_go_to_settings": "Ir para Configurações", + "reset_pin_go_to_settings_desc": "Em Configurações, selecione \"Segurança\" e depois \"Redefinir PIN\"", + "reset_pin_open_other_device": "Abrir outro dispositivo", + "reset_pin_open_other_device_desc": "Vá para outro dispositivo onde sua conta social está conectada", + "reset_pin_set_your_new_pin": "Defina o Seu Novo PIN", + "reset_pin_set_your_new_pin_desc": "Depois de definir o seu novo PIN, pode agora iniciar sessão na sua carteira neste dispositivo", + "reset_pin_using_another_device": "Redefinir PIN usando outro dispositivo", + "reset_pin_using_another_device_desc": "Por segurança, você só pode redefinir seu PIN em outros dispositivos onde está conectado", "scan.camera_access_denied": "Acesso à câmera negado", "scan.enable_camera_permissions": "OneKey requer acesso à câmera para escanear códigos QR. Por favor, vá para \"Configurações\" e habilite as permissões da câmera para usar este recurso.", "scan.grant_camera_access_in_expand_view": "Por favor, conceda acesso à câmera na visualização expandida.", diff --git a/packages/shared/src/locale/json/pt_BR.json b/packages/shared/src/locale/json/pt_BR.json index 8f3daba1b196..aae0ce39355f 100644 --- a/packages/shared/src/locale/json/pt_BR.json +++ b/packages/shared/src/locale/json/pt_BR.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "Alterar a conta principal para {account}?", "homescreen.format_supported": "O formato { token } não é compatível.", "hw_banner_description": "Proteja seu cripto com a carteira de hardware mais poderosa", + "i_have_done_these_steps": "Eu fiz essas etapas", "id.delete_double_check": "Tem certeza que deseja excluir OneKey ID?", "id.delete_onekey_id": "Excluir ID OneKey", "id.delete_onekey_id_desc": "Ao excluir seu ID OneKey, você perderá acesso aos seguintes benefícios. Essa ação é irreversível.", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "Sua conta de chave privada está pronta", "private_key_imported_feedback_title": "Importado com sucesso", "private_key_staty_on_device": "As chaves privadas permanecem no dispositivo", + "promode.limit_sell_for": "Vender por", "promode.swap_unsupported_message": "Este token não é suportado. Tente swap_actionTrocar ou bridge_actionPonte.", "promode.swap_unsupported_title": "Swap não suportado", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "Eu anotei a frase de recuperação", "reset_app_desc": "Isso irá deletar todos os dados que você criou no OneKey. Após garantir que você tem um backup adequado, digite \"RESET\" para redefinir o App", "reset_pin": "Redefinir PIN", + "reset_pin_go_to_settings": "Ir para Configurações", + "reset_pin_go_to_settings_desc": "Em Configurações, selecione \"Segurança\" e depois \"Redefinir PIN\"", + "reset_pin_open_other_device": "Abrir outro dispositivo", + "reset_pin_open_other_device_desc": "Vá para outro dispositivo onde sua conta social está conectada", + "reset_pin_set_your_new_pin": "Defina Seu Novo PIN", + "reset_pin_set_your_new_pin_desc": "Depois de definir seu novo PIN, você já pode fazer login na sua carteira neste dispositivo", + "reset_pin_using_another_device": "Redefinir PIN usando outro dispositivo", + "reset_pin_using_another_device_desc": "Por segurança, você só pode redefinir seu PIN em outros dispositivos onde está conectado", "scan.camera_access_denied": "Acesso à câmera negado", "scan.enable_camera_permissions": "OneKey requer acesso à câmera para escanear códigos QR. Por favor, vá para \"Configurações\" e habilite as permissões da câmera para usar este recurso.", "scan.grant_camera_access_in_expand_view": "Por favor, conceda acesso à câmera na visualização expandida.", diff --git a/packages/shared/src/locale/json/ru.json b/packages/shared/src/locale/json/ru.json index b564c5ade1cd..c4fc66acb3e2 100644 --- a/packages/shared/src/locale/json/ru.json +++ b/packages/shared/src/locale/json/ru.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "Переключить основной аккаунт на {account}?", "homescreen.format_supported": "Формат { token } не поддерживается.", "hw_banner_description": "Защитите свою криптовалюту с помощью самого мощного аппаратного кошелька", + "i_have_done_these_steps": "Я выполнил эти шаги", "id.delete_double_check": "Вы действительно хотите удалить OneKey ID?", "id.delete_onekey_id": "Удалить идентификатор OneKey", "id.delete_onekey_id_desc": "При удалении OneKey ID вы потеряете доступ к следующим преимуществам. Это действие необратимо.", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "Ваш аккаунт с приватным ключом готов", "private_key_imported_feedback_title": "Импортировано успешно", "private_key_staty_on_device": "Приватные ключи остаются на устройстве", + "promode.limit_sell_for": "Продать за", "promode.swap_unsupported_message": "Этот токен не поддерживается. Попробуйте swap_actionОбмен или bridge_actionМост.", "promode.swap_unsupported_title": "Обмен не поддерживается", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "Я записал фразу для восстановления", "reset_app_desc": "Это удалит все данные, которые вы создали в OneKey. После того как вы убедитесь, что у вас есть надлежащая резервная копия, введите \"RESET\", чтобы сбросить приложение", "reset_pin": "Сбросить ПИН-код", + "reset_pin_go_to_settings": "Перейти в Настройки", + "reset_pin_go_to_settings_desc": "В настройках выберите «Безопасность», а затем «Сбросить PIN-код»", + "reset_pin_open_other_device": "Открыть другое устройство", + "reset_pin_open_other_device_desc": "Перейдите на другое устройство, где выполнен вход в вашу социальную сеть", + "reset_pin_set_your_new_pin": "Установите новый PIN-код", + "reset_pin_set_your_new_pin_desc": "После установки нового PIN-кода вы можете войти в свой кошелек на этом устройстве", + "reset_pin_using_another_device": "Сбросить PIN-код с помощью другого устройства", + "reset_pin_using_another_device_desc": "В целях безопасности вы можете сбросить PIN-код только на других устройствах, где вы вошли в систему", "scan.camera_access_denied": "Доступ к камере запрещен", "scan.enable_camera_permissions": "OneKey требует доступа к камере для сканирования QR-кодов. Пожалуйста, перейдите в \"Настройки\" и включите разрешения для камеры, чтобы использовать эту функцию.", "scan.grant_camera_access_in_expand_view": "Пожалуйста, предоставьте доступ к камере в расширенном виде.", diff --git a/packages/shared/src/locale/json/th_TH.json b/packages/shared/src/locale/json/th_TH.json index 0e5742fe9dbc..cb018f35798c 100644 --- a/packages/shared/src/locale/json/th_TH.json +++ b/packages/shared/src/locale/json/th_TH.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "เปลี่ยนบัญชีหลักเป็น {account}?", "homescreen.format_supported": "รูปแบบ { token } ไม่ได้รับการรองรับ", "hw_banner_description": "รักษาความปลอดภัยสำหรับคริปโตของคุณด้วยกระเป๋าเงินฮาร์ดแวร์ที่ทรงพลังที่สุด", + "i_have_done_these_steps": "ฉันทำขั้นตอนเหล่านี้เสร็จแล้ว", "id.delete_double_check": "แน่ใจหรือไม่ว่าต้องการลบ OneKey ID?", "id.delete_onekey_id": "ลบ OneKey ID", "id.delete_onekey_id_desc": "หากลบ OneKey ID คุณจะสูญเสียสิทธิประโยชน์ดังต่อไปนี้ และไม่สามารถยกเลิกการดำเนินการนี้ได้", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "บัญชีคีย์ส่วนตัวของคุณพร้อมแล้ว", "private_key_imported_feedback_title": "นำเข้าเรียบร้อยแล้ว", "private_key_staty_on_device": "กุญแจส่วนตัวจะอยู่บนอุปกรณ์เท่านั้น", + "promode.limit_sell_for": "ขายในราคา", "promode.swap_unsupported_message": "โทเค็นนี้ไม่ได้รับการรองรับ ลอง swap_actionสลับ หรือ bridge_actionบริดจ์.", "promode.swap_unsupported_title": "ไม่รองรับการสวอป", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "ฉันได้เขียน recovery phrase ลงไปแล้ว", "reset_app_desc": "การดำเนินการนี้จะลบข้อมูลทั้งหมดที่คุณสร้างบน OneKey หลังจากที่คุณแน่ใจว่ามีการสำรองข้อมูลอย่างเหมาะสมแล้ว ให้ป้อน \"RESET\" เพื่อรีเซ็ตแอป", "reset_pin": "รีเซ็ต PIN", + "reset_pin_go_to_settings": "ไปที่การตั้งค่า", + "reset_pin_go_to_settings_desc": "ในการตั้งค่า ให้เลือก \"ความปลอดภัย\" แล้วจึงเลือก \"รีเซ็ตรหัส PIN\"", + "reset_pin_open_other_device": "เปิดอุปกรณ์อื่น", + "reset_pin_open_other_device_desc": "ไปยังอุปกรณ์อื่นที่บัญชีโซเชียลของคุณลงชื่อเข้าใช้อยู่", + "reset_pin_set_your_new_pin": "ตั้งรหัส PIN ใหม่ของคุณ", + "reset_pin_set_your_new_pin_desc": "เมื่อคุณตั้งรหัส PIN ใหม่เรียบร้อยแล้ว คุณสามารถเข้าสู่กระเป๋าเงินของคุณบนอุปกรณ์นี้ได้", + "reset_pin_using_another_device": "รีเซ็ตรหัส PIN โดยใช้ อุปกรณ์ อื่น", + "reset_pin_using_another_device_desc": "เพื่อความปลอดภัย คุณสามารถรีเซ็ตรหัส PIN ได้เฉพาะในอุปกรณ์อื่นที่คุณได้ลงชื่อเข้าใช้อยู่เท่านั้น", "scan.camera_access_denied": "การเข้าถึงกล้องถูกปฏิเสธ", "scan.enable_camera_permissions": "OneKey ต้องการการเข้าถึงกล้องเพื่อสแกน QR codes กรุณาไปที่ \"การตั้งค่า\" และเปิดใช้งานสิทธิ์การใช้กล้องเพื่อใช้คุณลักษณะนี้", "scan.grant_camera_access_in_expand_view": "โปรดอนุญาตให้เข้าถึงกล้องในมุมมองขยาย", diff --git a/packages/shared/src/locale/json/uk_UA.json b/packages/shared/src/locale/json/uk_UA.json index 4c667d0fe533..9db1a06119e2 100644 --- a/packages/shared/src/locale/json/uk_UA.json +++ b/packages/shared/src/locale/json/uk_UA.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "Переключити основний рахунок на {account}?", "homescreen.format_supported": "Формат { token } не підтримується.", "hw_banner_description": "Захистіть свою криптовалюту за допомогою найпотужнішого апаратного гаманця", + "i_have_done_these_steps": "Я виконав ці кроки", "id.delete_double_check": "Ви дійсно хочете видалити OneKey ID?", "id.delete_onekey_id": "Видалити OneKey ID", "id.delete_onekey_id_desc": "Видаливши OneKey ID, ви втратите доступ до наступних переваг. Цю дію неможливо скасувати.", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "Ваш обліковий запис з приватним ключем готовий", "private_key_imported_feedback_title": "Успішно імпортовано", "private_key_staty_on_device": "Приватні ключі залишаються на пристрої", + "promode.limit_sell_for": "Продати за", "promode.swap_unsupported_message": "Цей токен не підтримується. Спробуйте swap_actionОбмін або bridge_actionМіст.", "promode.swap_unsupported_title": "Обмін не підтримується", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "Я записав фразу відновлення", "reset_app_desc": "Це видалить всі дані, які ви створили на OneKey. Після перевірки наявності належної резервної копії, введіть \"RESET\", щоб скинути додаток", "reset_pin": "Скинути PIN-код", + "reset_pin_go_to_settings": "Перейти до Налаштувань", + "reset_pin_go_to_settings_desc": "У Налаштуваннях виберіть «Безпека», а потім «Скинути PIN-код»", + "reset_pin_open_other_device": "Відкрити інший пристрій", + "reset_pin_open_other_device_desc": "Перейдіть на інший пристрій, де ви ввійшли у свій обліковий запис соціальної мережі", + "reset_pin_set_your_new_pin": "Встановіть новий PIN-код", + "reset_pin_set_your_new_pin_desc": "Після встановлення нового PIN-коду ви зможете увійти до свого гаманця на цьому пристрої", + "reset_pin_using_another_device": "Скинути PIN-код за допомогою іншого пристрою", + "reset_pin_using_another_device_desc": "З міркувань безпеки ви можете скинути свій PIN-код лише на інших пристроях, де ви увійшли в систему", "scan.camera_access_denied": "Доступ до камери заборонено", "scan.enable_camera_permissions": "OneKey вимагає доступу до камери для сканування QR-кодів. Будь ласка, перейдіть до \"Налаштувань\" та включіть дозволи на камеру, щоб використовувати цю функцію.", "scan.grant_camera_access_in_expand_view": "Будь ласка, надайте доступ до камери у розгорнутому вигляді.", diff --git a/packages/shared/src/locale/json/vi.json b/packages/shared/src/locale/json/vi.json index 93edd36a70fe..9b43530c1cae 100644 --- a/packages/shared/src/locale/json/vi.json +++ b/packages/shared/src/locale/json/vi.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "Chuyển tài khoản chính sang {account}?", "homescreen.format_supported": "Định dạng { token } không được hỗ trợ.", "hw_banner_description": "Bảo mật tiền điện tử của bạn với ví cứng mạnh mẽ nhất", + "i_have_done_these_steps": "Tôi đã thực hiện các bước này", "id.delete_double_check": "Bạn chắc chắn muốn xóa OneKey ID?", "id.delete_onekey_id": "Xóa ID OneKey", "id.delete_onekey_id_desc": "Khi xóa OneKey ID, bạn sẽ mất quyền truy cập các quyền lợi sau đây. Hành động này không thể hoàn tác.", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "Tài khoản khóa riêng của bạn đã sẵn sàng", "private_key_imported_feedback_title": "Nhập thành công", "private_key_staty_on_device": "Khóa riêng tư được lưu trên thiết bị", + "promode.limit_sell_for": "Bán với giá", "promode.swap_unsupported_message": "Token này không được hỗ trợ, Hãy thử swap_actionHoán đổi hoặc bridge_actionCầu nối.", "promode.swap_unsupported_title": "Không hỗ trợ hoán đổi", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "Tôi đã ghi lại cụm từ khôi phục", "reset_app_desc": "Điều này sẽ xóa tất cả dữ liệu bạn đã tạo trên OneKey. Sau khi đảm bảo rằng bạn đã sao lưu đúng cách, nhập \"RESET\" để đặt lại Ứng dụng", "reset_pin": "Đặt lại mã PIN", + "reset_pin_go_to_settings": "Đi tới Cài đặt", + "reset_pin_go_to_settings_desc": "Trong Cài đặt, chọn \"Bảo mật\" rồi chọn \"Đặt lại mã PIN\"", + "reset_pin_open_other_device": "Mở thiết bị khác", + "reset_pin_open_other_device_desc": "Chuyển sang thiết bị khác mà tài khoản mạng xã hội của bạn đã đăng nhập", + "reset_pin_set_your_new_pin": "Đặt Mã PIN Mới", + "reset_pin_set_your_new_pin_desc": "Sau khi đã đặt mã PIN mới, bạn có thể đăng nhập vào ví của mình trên thiết bị này", + "reset_pin_using_another_device": "Đặt lại mã PIN bằng thiết bị khác", + "reset_pin_using_another_device_desc": "Để bảo mật, bạn chỉ có thể đặt lại mã PIN trên các thiết bị khác mà bạn đã đăng nhập", "scan.camera_access_denied": "Quyền truy cập camera bị từ chối", "scan.enable_camera_permissions": "OneKey yêu cầu quyền truy cập vào camera để quét mã QR. Vui lòng vào “Cài đặt” và bật quyền truy cập camera để sử dụng tính năng này.", "scan.grant_camera_access_in_expand_view": "Vui lòng cấp quyền truy cập camera trong chế độ xem mở rộng.", diff --git a/packages/shared/src/locale/json/zh_CN.json b/packages/shared/src/locale/json/zh_CN.json index 13368f8fc8af..ecd243e0a1d5 100644 --- a/packages/shared/src/locale/json/zh_CN.json +++ b/packages/shared/src/locale/json/zh_CN.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "将首要账户切换为{account}?", "homescreen.format_supported": "不支持{ token }格式。", "hw_banner_description": "使用迄今为止最强大的硬件钱包保护您的加密资产", + "i_have_done_these_steps": "我已完成以上步骤", "id.delete_double_check": "确定要删除 OneKey ID 吗?", "id.delete_onekey_id": "删除 OneKey ID", "id.delete_onekey_id_desc": "删除 OneKey ID 后,您将无法享受以下权益。此操作不可撤销。", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "您的私钥账户已准备就绪", "private_key_imported_feedback_title": "导入成功", "private_key_staty_on_device": "私钥保存在设备内", + "promode.limit_sell_for": "兑换为", "promode.swap_unsupported_message": "暂不支持此代币,请尝试swap_action兑换bridge_action跨链。", "promode.swap_unsupported_title": "暂不支持兑换", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "我已经写下了助记词", "reset_app_desc": "这将删除您在 OneKey 上创建的所有数据。确保您已妥善备份,输入「RESET」以重置 app。", "reset_pin": "重置 PIN 码", + "reset_pin_go_to_settings": "前往设置", + "reset_pin_go_to_settings_desc": "在「设置」中,选择「安全」,然后点击「重置 PIN」。", + "reset_pin_open_other_device": "打开另一台设备", + "reset_pin_open_other_device_desc": "前往另一台已登录你社交账号的设备", + "reset_pin_set_your_new_pin": "设置您的新 PIN 码", + "reset_pin_set_your_new_pin_desc": "新 PIN 设置完成,现在可以在此设备上登录你的钱包。", + "reset_pin_using_another_device": "使用另一台设备重置 PIN", + "reset_pin_using_another_device_desc": "出于安全考虑,你只能在已登录的其他设备上重置 PIN", "scan.camera_access_denied": "没有摄像头访问权限", "scan.enable_camera_permissions": "OneKey 需要使用摄像头来扫描二维码,请前往“设置”并启用摄像头权限以使用此功能。", "scan.grant_camera_access_in_expand_view": "请在展开视图中授权相机访问", diff --git a/packages/shared/src/locale/json/zh_HK.json b/packages/shared/src/locale/json/zh_HK.json index c7e74e5904e5..cf7bc3bfba61 100644 --- a/packages/shared/src/locale/json/zh_HK.json +++ b/packages/shared/src/locale/json/zh_HK.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "將首要帳戶切換至{account}?", "homescreen.format_supported": "不支援{ token }格式。", "hw_banner_description": "使用迄今為止最強大的硬體錢包保護您的加密資產", + "i_have_done_these_steps": "我已完成以上步驟", "id.delete_double_check": "確定要刪除 OneKey ID 嗎?", "id.delete_onekey_id": "刪除 OneKey ID", "id.delete_onekey_id_desc": "刪除 OneKey ID 後,您將無法享受以下權益。此操作不可撤銷。", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "您的私鑰帳戶已準備就緒", "private_key_imported_feedback_title": "匯入成功", "private_key_staty_on_device": "私密金鑰保留在裝置上", + "promode.limit_sell_for": "兌換為", "promode.swap_unsupported_message": "此代幣不受支援,請嘗試swap_action兌換bridge_action橋接。", "promode.swap_unsupported_title": "不支援交換", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "我已經寫下了助記詞", "reset_app_desc": "這將刪除您在 OneKey 上創建的所有數據。確保您已妥善備份,輸入「RESET」以重置 app。", "reset_pin": "重設 PIN 碼", + "reset_pin_go_to_settings": "前往設定", + "reset_pin_go_to_settings_desc": "在「設定」中,選擇「安全」,然後點選「重設 PIN」。", + "reset_pin_open_other_device": "開啟另一台裝置", + "reset_pin_open_other_device_desc": "前往另一台已登入你社群帳號的設備", + "reset_pin_set_your_new_pin": "設定您的新 PIN 碼", + "reset_pin_set_your_new_pin_desc": "新 PIN 設定完成,現在可以在此裝置上登入你的錢包。", + "reset_pin_using_another_device": "使用另一台裝置重設 PIN", + "reset_pin_using_another_device_desc": "出於安全考慮,你只能在已登入的其他裝置上重置 PIN", "scan.camera_access_denied": "沒有攝像頭訪問權限", "scan.enable_camera_permissions": "OneKey 需要使用攝像頭來掃描二維碼,請前往“設置”並啓用攝像頭權限以使用此功能。", "scan.grant_camera_access_in_expand_view": "請在展開視圖中授予相機訪問權限", diff --git a/packages/shared/src/locale/json/zh_TW.json b/packages/shared/src/locale/json/zh_TW.json index 22845eddc45a..196f552421cd 100644 --- a/packages/shared/src/locale/json/zh_TW.json +++ b/packages/shared/src/locale/json/zh_TW.json @@ -1951,6 +1951,7 @@ "history.switch_account_dialog_title": "將首要帳戶切換至{account}?", "homescreen.format_supported": "不支援{ token }格式。", "hw_banner_description": "使用迄今為止最強大的硬體錢包保護您的加密資產", + "i_have_done_these_steps": "我已完成以上步驟", "id.delete_double_check": "確定要刪除 OneKey ID 嗎?", "id.delete_onekey_id": "刪除 OneKey ID", "id.delete_onekey_id_desc": "刪除 OneKey ID 後,您將無法享受以下權益。此操作不可撤銷。", @@ -2782,6 +2783,7 @@ "private_key_imported_feedback_desc": "您的私鑰帳戶已準備就緒", "private_key_imported_feedback_title": "匯入成功", "private_key_staty_on_device": "私鑰保留在裝置上", + "promode.limit_sell_for": "兌換為", "promode.swap_unsupported_message": "此代幣不受支援,請嘗試swap_action兌換bridge_action橋接。", "promode.swap_unsupported_title": "不支援交換", "promode.value_ws": "Value (USD)", @@ -3054,6 +3056,14 @@ "remove_wallet_double_confirm_message": "我已經寫下了助記詞", "reset_app_desc": "這將刪除您在OneKey上創建的所有數據。確保您已經妥善備份後,輸入\"RESET\"以重置應用程式", "reset_pin": "重設 PIN 碼", + "reset_pin_go_to_settings": "前往設定", + "reset_pin_go_to_settings_desc": "在「設定」中,選擇「安全」,然後點選「重設 PIN」。", + "reset_pin_open_other_device": "開啟另一台裝置", + "reset_pin_open_other_device_desc": "前往另一台已登入你社群帳號的設備", + "reset_pin_set_your_new_pin": "設定您的新 PIN 碼", + "reset_pin_set_your_new_pin_desc": "新 PIN 設定完成,現在可以在此裝置上登入你的錢包。", + "reset_pin_using_another_device": "使用另一台裝置重設 PIN", + "reset_pin_using_another_device_desc": "出於安全考慮,你只能在已登入的其他裝置上重置 PIN", "scan.camera_access_denied": "沒有攝像頭訪問權限", "scan.enable_camera_permissions": "OneKey 需要使用攝像頭來掃描二維碼,請前往“設置”並啓用攝像頭權限以使用此功能。", "scan.grant_camera_access_in_expand_view": "請在展開視圖中授予相機訪問權限", From a6c54378c933bf1e6513e19ae71bf7ceb27e8f12 Mon Sep 17 00:00:00 2001 From: morizon Date: Sat, 27 Dec 2025 22:25:54 +0800 Subject: [PATCH 31/66] feat: enhance Keyless Wallet V2 with new onboarding features and debug panel for custom mnemonic management --- .../ServiceKeylessWallet.ts | 240 +++++++++++++++-- .../src/services/ServicePassword/index.ts | 5 + .../KeylessWallet/useKeylessWallet.tsx | 243 ++++++++++++++---- .../Password/components/PasswordVerify.tsx | 5 + .../container/PasswordVerifyContainer.tsx | 11 +- packages/kit/src/routes/Modal/router.tsx | 2 + .../contexts/accountSelector/actions.tsx | 39 +-- .../Components/stories/OneKeyIDGallery.tsx | 25 ++ .../Onboarding/pages/FinalizeWalletSetup.tsx | 12 +- .../components/PinInputLayout.tsx | 14 +- .../Onboardingv2/pages/ConfirmPinPage.tsx | 60 +++-- .../pages/CreateOrImportWallet.tsx | 6 +- .../Onboardingv2/pages/CreatePasscodePage.tsx | 104 ++++++-- .../Onboardingv2/pages/CreatePinPage.tsx | 4 +- .../pages/FinalizeWalletSetup.tsx | 3 + .../pages/KeylessOnboardingDebugPanel.tsx | 93 +++++++ .../Onboardingv2/pages/OneKeyIDLoginPage.tsx | 27 +- .../Onboardingv2/pages/VerifyPinPage.tsx | 37 ++- .../src/keylessWallet/keylessWalletConsts.ts | 4 + .../src/keylessWallet/keylessWalletTypes.ts | 6 + packages/shared/src/routes/onboarding.ts | 3 +- packages/shared/src/routes/onboardingv2.ts | 9 +- 22 files changed, 762 insertions(+), 190 deletions(-) create mode 100644 packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx diff --git a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts index 8394860f629b..ecc19b1e33b3 100644 --- a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts +++ b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts @@ -2,6 +2,7 @@ import { isEqual } from 'lodash'; import { LRUCache } from 'lru-cache'; import { + decryptStringAsync, encryptAsync, encryptStringAsync, generateMnemonic, @@ -21,6 +22,7 @@ import type { ICloudKeyPack, IDeviceKeyPack, IKeylessBackendShare, + IKeylessJuiceboxShare, IKeylessMnemonicInfo, IKeylessWalletPacks, IKeylessWalletRestoredData, @@ -31,6 +33,7 @@ import keylessWalletUtils from '@onekeyhq/shared/src/keylessWallet/keylessWallet import shamirUtils from '@onekeyhq/shared/src/keylessWallet/shamirUtils'; import { appLocale } from '@onekeyhq/shared/src/locale/appLocale'; import { ETranslations } from '@onekeyhq/shared/src/locale/enum/translations'; +import appStorage from '@onekeyhq/shared/src/storage/appStorage'; import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; import bufferUtils from '@onekeyhq/shared/src/utils/bufferUtils'; import type { IAvatarInfo } from '@onekeyhq/shared/src/utils/emojiUtils'; @@ -1081,16 +1084,90 @@ class ServiceKeylessWallet extends ServiceBase { } } + private static readonly MOCKED_KEYLESS_SHARES_STORAGE_KEY = + 'keyless_mocked_shares'; + + private async getMockedKeylessShares(): Promise<{ + [ownerId: string]: { + backendShare: IKeylessBackendShare | undefined; + juiceboxShare: IKeylessJuiceboxShare | undefined; + }; + }> { + const data = await appStorage.getItem( + ServiceKeylessWallet.MOCKED_KEYLESS_SHARES_STORAGE_KEY, + ); + if (!data) { + return {}; + } + try { + return JSON.parse(data) as { + [ownerId: string]: { + backendShare: IKeylessBackendShare | undefined; + juiceboxShare: IKeylessJuiceboxShare | undefined; + }; + }; + } catch { + return {}; + } + } + + private async saveMockedKeylessShares(shares: { + [ownerId: string]: { + backendShare: IKeylessBackendShare | undefined; + juiceboxShare: IKeylessJuiceboxShare | undefined; + }; + }): Promise { + await appStorage.setItem( + ServiceKeylessWallet.MOCKED_KEYLESS_SHARES_STORAGE_KEY, + JSON.stringify(shares), + ); + } + + @backgroundMethod() + @toastIfError() + async clearMockedKeylessShares(): Promise { + await appStorage.removeItem( + ServiceKeylessWallet.MOCKED_KEYLESS_SHARES_STORAGE_KEY, + ); + } + + buildKeylessOwnerIdFromSocialToken(params: { token: string }): string { + const { token } = params; + const decodedToken = stringUtils.decodeJWT(token) as ISupabaseJWTPayload; + const provider = decodedToken?.app_metadata?.provider || ''; + const socialAccountId = decodedToken?.user_metadata?.sub || ''; + return `${provider}:${socialAccountId}`; + } + @backgroundMethod() @toastIfError() async apiGetKeylessBackendShare(params: { token: string; }): Promise { + const { token } = params; // verify token by supabase SDK, make sure token is valid and generated by Google or Apple // decode token to get social account id // hash social account id by env salt and KMS - const socialAccountId = ''; - return null; + const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); + // TODO: Replace with real API call + // For now, return from mock cache + const mockedShares = await this.getMockedKeylessShares(); + return mockedShares[ownerId]?.backendShare || null; + } + + @backgroundMethod() + @toastIfError() + async apiGetKeylessJuiceboxShare(params: { + token: string; + }): Promise { + const { token } = params; + const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); + // TODO: Replace with real API call + // exchange juicebox token from onekey auth server + // get juicebox share from juicebox network + // For now, return from mock cache + const mockedShares = await this.getMockedKeylessShares(); + return mockedShares[ownerId]?.juiceboxShare || null; } @backgroundMethod() @@ -1101,33 +1178,157 @@ class ServiceKeylessWallet extends ServiceBase { backendShare: string; }): Promise { const { token, encryptedMnemonic, backendShare } = params; - const decodedToken = stringUtils.decodeJWT(token) as ISupabaseJWTPayload; - const provider = decodedToken?.app_metadata?.provider || ''; - const socialAccountId = decodedToken?.user_metadata?.sub || ''; - const ownerId = `${provider}:${socialAccountId}`; - return { + const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); + // TODO: Replace with real API call + // For now, save to mock cache + const backendShareData: IKeylessBackendShare = { ownerId, encryptedMnemonic, backendShare, }; + const mockedShares = await this.getMockedKeylessShares(); + if (!mockedShares[ownerId]) { + mockedShares[ownerId] = {} as { + backendShare: IKeylessBackendShare; + juiceboxShare: IKeylessJuiceboxShare; + }; + } + mockedShares[ownerId].backendShare = backendShareData; + await this.saveMockedKeylessShares(mockedShares); + return backendShareData; } + @backgroundMethod() + @toastIfError() async apiUploadKeylessJuiceboxShare(params: { token: string; + pin: string; juiceboxShare: string; - }): Promise<{ success: boolean }> { + }): Promise { + const { token, pin, juiceboxShare } = params; + const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); + // TODO: Replace with real API call // exchange juicebox token from onekey auth server // upload juicebox share to juicebox network + // For now, save to mock cache + const juiceboxShareData: IKeylessJuiceboxShare = { + ownerId, + pin, + juiceboxShare, + }; + const mockedShares = await this.getMockedKeylessShares(); + if (!mockedShares[ownerId]) { + mockedShares[ownerId] = {} as { + backendShare: IKeylessBackendShare; + juiceboxShare: IKeylessJuiceboxShare; + }; + } + mockedShares[ownerId].juiceboxShare = juiceboxShareData; + await this.saveMockedKeylessShares(mockedShares); + return juiceboxShareData; + } + + // apiVerifyKeylessJuiceboxPin + @backgroundMethod() + @toastIfError() + async apiVerifyKeylessJuiceboxPin(params: { token: string; pin: string }) { + await timerUtils.wait(1500); + const { token, pin } = params; + const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); + // TODO: Replace with real API call + // For now, verify PIN from mock cache + const mockedShares = await this.getMockedKeylessShares(); + const juiceboxShare = mockedShares[ownerId]?.juiceboxShare; + if (!juiceboxShare || juiceboxShare?.pin !== pin) { + throw new OneKeyLocalError('Invalid PIN'); + } + } + + @backgroundMethod() + @toastIfError() + async restoreKeylessWalletFromServer(params: { + token: string | undefined; + pin: string | undefined; + }) { + const { token, pin } = params; + if (!token) { + throw new OneKeyLocalError('social login token is required'); + } + if (!pin) { + throw new OneKeyLocalError('pin is required'); + } + const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); + const mockedShares = await this.getMockedKeylessShares(); + const existingShares = mockedShares[ownerId]; + if (!existingShares?.backendShare || !existingShares?.juiceboxShare) { + throw new OneKeyLocalError('Keyless wallet not initialized'); + } + + // Verify PIN + await this.apiVerifyKeylessJuiceboxPin({ token, pin }); + + // Get shares from server + const backendShareData = await this.apiGetKeylessBackendShare({ token }); + if (!backendShareData) { + throw new OneKeyLocalError('Backend share not found'); + } + const juiceboxShareData = await this.apiGetKeylessJuiceboxShare({ token }); + if (!juiceboxShareData) { + throw new OneKeyLocalError('Juicebox share not found'); + } + + // Combine shares to recover mnemonic password + const mnemonicPasswordShares = [ + bufferUtils.base64ToBytes(backendShareData.backendShare), + bufferUtils.base64ToBytes(juiceboxShareData.juiceboxShare), + ]; + const mnemonicPasswordBytes = await shamirUtils.combine( + mnemonicPasswordShares.map((s) => new Uint8Array(s)), + ); + const mnemonicPassword = bufferUtils.bytesToBase64(mnemonicPasswordBytes); + + // Decrypt mnemonic using recovered password + const mnemonic = await decryptStringAsync({ + data: backendShareData.encryptedMnemonic, + dataEncoding: 'hex', + resultEncoding: 'utf-8', + password: mnemonicPassword, + allowRawPassword: true, + iterations: 600_000, + }); + return { - success: true, + mnemonic: await this.backgroundApi.servicePassword.encodeSensitiveText({ + text: mnemonic, + }), }; } @backgroundMethod() @toastIfError() - async createKeylessWalletV2(params: { token: string }) { - const { token } = params; - const mnemonic: string = generateMnemonic(256); + async initKeylessWalletToServer(params: { + token: string | undefined; + pin: string | undefined; + customMnemonic?: string; + }) { + const { token, pin, customMnemonic } = params; + if (!token) { + throw new OneKeyLocalError('social login token is required'); + } + if (!pin) { + throw new OneKeyLocalError('pin is required'); + } + const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); + const mockedShares = await this.getMockedKeylessShares(); + const existingShares = mockedShares[ownerId]; + if (existingShares?.backendShare || existingShares?.juiceboxShare) { + throw new OneKeyLocalError('Keyless wallet already initialized'); + } + let mnemonic: string = generateMnemonic(256); + const devSettings = await devSettingsPersistAtom.get(); + if (devSettings.enabled && customMnemonic && customMnemonic.trim()) { + mnemonic = customMnemonic.trim(); + } const mnemonicPasswordBytes = crypto.getRandomValues(new Uint8Array(32)); const mnemonicPassword = bufferUtils.bytesToBase64(mnemonicPasswordBytes); const encryptedMnemonic: string = await encryptStringAsync({ @@ -1157,15 +1358,20 @@ class ServiceKeylessWallet extends ServiceBase { encryptedMnemonic, backendShare, }); - const juiceboxShareData: { success: boolean } = + // TODO verify backendShareData is valid + const juiceboxShareData: IKeylessJuiceboxShare = await this.apiUploadKeylessJuiceboxShare({ token, juiceboxShare, + pin, }); - return this.backgroundApi.serviceAccount.createHDWallet({ - mnemonic, - isWalletBackedUp: true, - }); + // TODO verify juiceboxShareData is valid + + return { + mnemonic: await this.backgroundApi.servicePassword.encodeSensitiveText({ + text: mnemonic, + }), + }; } } diff --git a/packages/kit-bg/src/services/ServicePassword/index.ts b/packages/kit-bg/src/services/ServicePassword/index.ts index 394c452ada5c..e89d00af4e0a 100644 --- a/packages/kit-bg/src/services/ServicePassword/index.ts +++ b/packages/kit-bg/src/services/ServicePassword/index.ts @@ -249,6 +249,11 @@ export default class ServicePassword extends ServiceBase { return this.cachedPassword; } + @backgroundMethod() + async hasCachedPassword(): Promise { + return !!this.cachedPassword; + } + @backgroundMethod() async getCachedPasswordOrDeviceParams({ walletId }: { walletId: string }) { const isHardware = accountUtils.isHwWallet({ walletId }); diff --git a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx index f252593e9034..aa56e1f62f0f 100644 --- a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx +++ b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx @@ -4,14 +4,20 @@ import { useIntl } from 'react-intl'; import { Dialog } from '@onekeyhq/components'; import { primePersistAtom } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; -import { useDevSettingsPersistAtom } from '@onekeyhq/kit-bg/src/states/jotai/atoms/devSettings'; +import { + devSettingsPersistAtom, + useDevSettingsPersistAtom, +} from '@onekeyhq/kit-bg/src/states/jotai/atoms/devSettings'; import { EPrimeEmailOTPScene } from '@onekeyhq/shared/src/consts/primeConsts'; import { OneKeyLocalError, PrimeSendEmailOTPCancelError, } from '@onekeyhq/shared/src/errors'; import errorToastUtils from '@onekeyhq/shared/src/errors/utils/errorToastUtils'; -import { EKeylessWalletEnableScene } from '@onekeyhq/shared/src/keylessWallet/keylessWalletConsts'; +import { + EKeylessFinalizeAction, + EKeylessWalletEnableScene, +} from '@onekeyhq/shared/src/keylessWallet/keylessWalletConsts'; import type { IAuthKeyPack, ICloudKeyPack, @@ -273,20 +279,79 @@ export function useKeylessWalletMethods() { }; } -const keylessOnboardingCache = new cacheUtils.LRUCache({ +export const keylessOnboardingCache = new cacheUtils.LRUCache({ max: 1000, ttl: timerUtils.getTimeDurationMs({ minute: 3 }), ttlAutopurge: true, }); -function keylessOnboardingCacheGetAndDelete(key: string) { +async function keylessOnboardingCacheGetAndDelete( + key: string, + options: { + skipDelete?: boolean; + } = {}, +) { const token = keylessOnboardingCache.get(key); - keylessOnboardingCache.delete(key); + if (!options?.skipDelete) { + keylessOnboardingCache.delete(key); + } + if (!token) { + return ''; + } + return backgroundApiProxy.servicePassword.decodeSensitiveText({ + encodedText: token, + }); +} + +async function keylessOnboardingCacheSet(key: string, value: string) { + keylessOnboardingCache.set( + key, + await backgroundApiProxy.servicePassword.encodeSensitiveText({ + text: value, + }), + ); +} + +async function cacheKeylessOnboardingToken({ token }: { token: string }) { + await keylessOnboardingCacheSet('socialLoginToken', token); +} + +async function getKeylessOnboardingToken(options?: { skipDelete?: boolean }) { + const token = keylessOnboardingCacheGetAndDelete('socialLoginToken', options); return token; } -function keylessOnboardingCacheSet(key: string, value: string) { - keylessOnboardingCache.set(key, value); +async function cacheKeylessOnboardingPin({ pin }: { pin: string }) { + await keylessOnboardingCacheSet('onboardingPin', pin); +} + +async function getKeylessOnboardingPin(options?: { skipDelete?: boolean }) { + const pin = keylessOnboardingCacheGetAndDelete('onboardingPin', options); + return pin; +} + +async function cacheKeylessOnboardingCustomMnemonic({ + customMnemonic, +}: { + customMnemonic: string; +}) { + const devSettings = await devSettingsPersistAtom.get(); + if (devSettings.enabled) { + await keylessOnboardingCacheSet('customMnemonic', customMnemonic); + } +} + +async function getKeylessOnboardingCustomMnemonic(options?: { + skipDelete?: boolean; +}) { + const devSettings = await devSettingsPersistAtom.get(); + if (devSettings.enabled) { + const customMnemonic = keylessOnboardingCacheGetAndDelete( + 'customMnemonic', + options, + ); + return customMnemonic; + } } if (process.env.NODE_ENV !== 'production') { @@ -435,7 +500,20 @@ export function useKeylessWallet() { ], ); - const checkKeylessWalletExistence = useCallback(async () => { + const handleKeylessOnboardingTimeout = useCallback(() => { + Dialog.show({ + title: 'Keyless Wallet', + description: 'Keyless Wallet onboarding timed out. Please try again.', + showCancelButton: false, + onConfirmText: intl.formatMessage({ + id: ETranslations.global_got_it, + }), + }); + throw new OneKeyLocalError('Keyless Wallet onboarding timed out'); + }, [intl]); + + // Renamed function, checks if KeylessWallet exists locally + const checkKeylessWalletLocalExistence = useCallback(async () => { if (enableKeylessWalletLoadingRef.current) { return; } @@ -451,7 +529,7 @@ export function useKeylessWallet() { title: 'Keyless Wallet', // TODO @franco 本地已经添加无私钥钱包,如果需要使用其他无私钥钱包,请先删除当前钱包 description: - 'You already have a Keyless Wallet on this device. No need to create another one.', + 'A Keyless Wallet is already added. To use another Keyless Wallet, please delete the current one first.', showCancelButton: false, onConfirmText: intl.formatMessage({ id: ETranslations.global_got_it, @@ -463,7 +541,7 @@ export function useKeylessWallet() { params: { screen: EOnboardingPagesV2.OneKeyIDLogin, params: { - mode: EOnboardingV2OneKeyIDLoginMode.CreateKeylessWallet, + mode: EOnboardingV2OneKeyIDLoginMode.CreateOrImportKeylessWallet, }, }, }); @@ -474,70 +552,139 @@ export function useKeylessWallet() { }); }, [intl, navigation]); - const checkKeylessWalletCreatedOnServer = useCallback( + const checkKeylessWalletInitedOnServer = useCallback( async ({ token }: { token: string }) => { + if (!token) { + handleKeylessOnboardingTimeout(); + return; + } const backendShareInfo = await backgroundApiProxy.serviceKeylessWallet.apiGetKeylessBackendShare( { token, }, ); - return { - isCreated: !!backendShareInfo, - backendShareInfo, - }; + const isInited = !!backendShareInfo; + await cacheKeylessOnboardingToken({ token }); + if (isInited) { + navigation.push(EOnboardingPagesV2.VerifyPin); + } else { + navigation.push(EOnboardingPagesV2.CreatePin); + } }, - [], + [handleKeylessOnboardingTimeout, navigation], ); - const createOrRestoreKeylessWallet = useCallback( - async ({ token }: { token: string }) => { - const { isCreated, backendShareInfo } = - await checkKeylessWalletCreatedOnServer({ token }); - if (isCreated) { - // TODO restore keyless wallet from server - console.log('backendShareInfo', backendShareInfo); - } else { - await actions.current.createKeylessWalletV2({ - token, + const finalizeKeylessWalletV2 = useCallback( + async ({ action }: { action: EKeylessFinalizeAction }) => { + const token = await getKeylessOnboardingToken(); + if (!token) { + handleKeylessOnboardingTimeout(); + return; + } + const pin = await getKeylessOnboardingPin(); + if (!pin) { + handleKeylessOnboardingTimeout(); + return; + } + if (!action) { + Dialog.show({ + title: 'Keyless Wallet', + description: 'EKeylessFinalizeAction is required', + showCancelButton: false, + onConfirmText: intl.formatMessage({ + id: ETranslations.global_got_it, + }), }); + return; + } + let mnemonic = ''; + if (action === EKeylessFinalizeAction.Create) { + const customMnemonic = await getKeylessOnboardingCustomMnemonic(); + ({ mnemonic } = + await backgroundApiProxy.serviceKeylessWallet.initKeylessWalletToServer( + { + token, + pin, + customMnemonic, + }, + )); + } + if (action === EKeylessFinalizeAction.Restore) { + ({ mnemonic } = + await backgroundApiProxy.serviceKeylessWallet.restoreKeylessWalletFromServer( + { + token, + pin, + }, + )); } + navigation.push(EOnboardingPagesV2.FinalizeWalletSetup, { + mnemonic, + isWalletBackedUp: true, + isKeylessWallet: true, + }); }, - [actions, checkKeylessWalletCreatedOnServer], + [navigation, handleKeylessOnboardingTimeout, intl], ); - const cacheKeylessOnboardingToken = useCallback( - async ({ token }: { token: string }) => { - keylessOnboardingCacheSet('socialLoginToken', token); + const confirmKeylessOnboardingPin = useCallback( + async ({ + pin, + action, + }: { + pin: string; + action: EKeylessFinalizeAction; + }) => { + await cacheKeylessOnboardingPin({ pin }); + const hasCachedPassword = + await backgroundApiProxy.servicePassword.hasCachedPassword(); + if (hasCachedPassword) { + await finalizeKeylessWalletV2({ action }); + } else { + navigation.push(EOnboardingPagesV2.CreatePasscode, { action }); + } }, - [], + [finalizeKeylessWalletV2, navigation], ); - const getKeylessOnboardingToken = useCallback(async () => { - const token = keylessOnboardingCacheGetAndDelete('socialLoginToken'); - return token; - }, []); - - const cacheKeylessOnboardingPin = useCallback(({ pin }: { pin: string }) => { - keylessOnboardingCacheSet('onboardingPin', pin); - }, []); - const getKeylessOnboardingPin = useCallback(() => { - const pin = keylessOnboardingCacheGetAndDelete('onboardingPin'); - return pin; - }, []); + const verifyKeylessOnboardingPin = useCallback( + async ({ pin }: { pin: string }) => { + const token = await getKeylessOnboardingToken({ skipDelete: true }); + if (!token) { + handleKeylessOnboardingTimeout(); + return; + } + await backgroundApiProxy.serviceKeylessWallet.apiVerifyKeylessJuiceboxPin( + { + token, + pin, + }, + ); + await cacheKeylessOnboardingToken({ token }); + await confirmKeylessOnboardingPin({ + pin, + action: EKeylessFinalizeAction.Restore, + }); + }, + [confirmKeylessOnboardingPin, handleKeylessOnboardingTimeout], + ); return { ...methods, // TODO handleKeylessWalletClick enableKeylessWallet, - checkKeylessWalletExistence, - checkKeylessWalletCreatedOnServer, - createOrRestoreKeylessWallet, enableKeylessWalletLoading, + checkKeylessWalletLocalExistence, // step1 + checkKeylessWalletInitedOnServer, // step2 + confirmKeylessOnboardingPin, // step3 + verifyKeylessOnboardingPin, + finalizeKeylessWalletV2, // step4 keylessOnboardingCache, - cacheKeylessOnboardingToken, - getKeylessOnboardingToken, cacheKeylessOnboardingPin, getKeylessOnboardingPin, + handleKeylessOnboardingTimeout, + cacheKeylessOnboardingCustomMnemonic, + getKeylessOnboardingCustomMnemonic, }; } diff --git a/packages/kit/src/components/Password/components/PasswordVerify.tsx b/packages/kit/src/components/Password/components/PasswordVerify.tsx index 24191aa38ab3..6d6cae77817e 100644 --- a/packages/kit/src/components/Password/components/PasswordVerify.tsx +++ b/packages/kit/src/components/Password/components/PasswordVerify.tsx @@ -57,6 +57,7 @@ interface IPasswordVerifyProps { }; alertText?: string; confirmBtnDisabled?: boolean; + pageMode?: boolean; } export interface IPasswordVerifyForm { @@ -65,6 +66,7 @@ export interface IPasswordVerifyForm { } function PasswordVerify({ + pageMode, isEnable, alertText, confirmBtnDisabled, @@ -346,6 +348,9 @@ function PasswordVerify({ form.setValue('passCode', pin); form.clearErrors('passCode'); setPassCodeClear(false); + if (pageMode) { + onPasswordChange(pin); + } }} editable={Boolean( status.value !== EPasswordVerifyStatus.VERIFYING && diff --git a/packages/kit/src/components/Password/container/PasswordVerifyContainer.tsx b/packages/kit/src/components/Password/container/PasswordVerifyContainer.tsx index b23466fff6b6..d098b0e143ae 100644 --- a/packages/kit/src/components/Password/container/PasswordVerifyContainer.tsx +++ b/packages/kit/src/components/Password/container/PasswordVerifyContainer.tsx @@ -45,12 +45,14 @@ interface IPasswordVerifyProps { onVerifyRes: (password: string) => void; onLayout?: (e: LayoutChangeEvent) => void; name?: 'lock'; + pageMode?: boolean; } const PasswordVerifyContainer = ({ onVerifyRes, onLayout, name, + pageMode, }: IPasswordVerifyProps) => { const intl = useIntl(); const [{ authType, isEnable }] = usePasswordBiologyAuthInfoAtom(); @@ -185,7 +187,8 @@ const PasswordVerifyContainer = ({ async (isExtLockNoCachePassword: boolean) => { if ( passwordVerifyStatus.value === EPasswordVerifyStatus.VERIFYING || - passwordVerifyStatus.value === EPasswordVerifyStatus.VERIFIED + (!pageMode && + passwordVerifyStatus.value === EPasswordVerifyStatus.VERIFIED) ) { return; } @@ -285,6 +288,7 @@ const PasswordVerifyContainer = ({ title, verifiedPasswordWebAuth, verifyPeriodBiologyAuthAttempts, + pageMode, ], ); @@ -294,7 +298,8 @@ const PasswordVerifyContainer = ({ async (data: IPasswordVerifyForm) => { if ( passwordVerifyStatus.value === EPasswordVerifyStatus.VERIFYING || - passwordVerifyStatus.value === EPasswordVerifyStatus.VERIFIED + (!pageMode && + passwordVerifyStatus.value === EPasswordVerifyStatus.VERIFIED) ) { return; } @@ -394,6 +399,7 @@ const PasswordVerifyContainer = ({ setPasswordPersist, setUnlockPeriodPasswordArray, unlockPeriodPasswordArray, + pageMode, ], ); @@ -429,6 +435,7 @@ const PasswordVerifyContainer = ({ return ( { + void keylessOnboardingCache.clear(); await v4migrationAtom.set((v) => ({ ...v, isProcessing: false, diff --git a/packages/kit/src/states/jotai/contexts/accountSelector/actions.tsx b/packages/kit/src/states/jotai/contexts/accountSelector/actions.tsx index 901e6236223b..c1c5b963e82e 100644 --- a/packages/kit/src/states/jotai/contexts/accountSelector/actions.tsx +++ b/packages/kit/src/states/jotai/contexts/accountSelector/actions.tsx @@ -844,9 +844,11 @@ class AccountSelectorActions extends ContextJotaiActionsBase { { mnemonic, isWalletBackedUp, + isKeylessWallet, }: { mnemonic: string; isWalletBackedUp?: boolean; + isKeylessWallet?: boolean; }, ) => this.withFinalizeWalletSetupStep.call(set, { @@ -855,6 +857,7 @@ class AccountSelectorActions extends ContextJotaiActionsBase { await serviceAccount.createHDWallet({ mnemonic, isWalletBackedUp, + isKeylessWallet, }); await this.autoSelectToCreatedWallet.call(set, { wallet, @@ -910,40 +913,6 @@ class AccountSelectorActions extends ContextJotaiActionsBase { }), ); - createKeylessWalletV2 = contextAtomMethod( - async ( - _, - set, - { - token, - }: { - token: string; - }, - ) => - this.withFinalizeWalletSetupStep.call(set, { - createWalletFn: async () => { - const { wallet, indexedAccount, isOverrideWallet } = - await backgroundApiProxy.serviceKeylessWallet.createKeylessWalletV2( - { - token, - }, - ); - await this.autoSelectToCreatedWallet.call(set, { - wallet, - indexedAccount, - isOverrideWallet, - }); - return { wallet, indexedAccount, isOverrideWallet }; - }, - generatingAccountsFn: async ({ wallet, indexedAccount }) => { - await this.addDefaultNetworkAccounts.call(set, { - wallet, - indexedAccount, - }); - }, - }), - ); - createHWWallet = contextAtomMethod( async ( _, @@ -2365,7 +2334,6 @@ export function useAccountSelectorActions() { const createQrWallet = actions.createQrWallet.use(); const createTonImportedWallet = actions.createTonImportedWallet.use(); const createKeylessWallet = actions.createKeylessWallet.use(); - const createKeylessWalletV2 = actions.createKeylessWalletV2.use(); const autoSelectNextAccount = actions.autoSelectNextAccount.use(); const updateHwWalletsDeprecatedStatus = actions.updateHwWalletsDeprecatedStatus.use(); @@ -2405,7 +2373,6 @@ export function useAccountSelectorActions() { createQrWallet, createTonImportedWallet, createKeylessWallet, - createKeylessWalletV2, updateHwWalletsDeprecatedStatus, autoSelectNextAccount, autoSelectNetworkOfOthersWalletAccount, diff --git a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx index 07e7d15d0676..11e91a4f497d 100644 --- a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx +++ b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx @@ -17,12 +17,18 @@ import { import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; import { useSupabaseAuth } from '@onekeyhq/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth'; import { useOneKeyAuth } from '@onekeyhq/kit/src/components/OneKeyAuth/useOneKeyAuth'; +import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; import { EOAuthSocialLoginProvider, SUPABASE_PROJECT_URL, SUPABASE_PUBLIC_API_KEY, } from '@onekeyhq/shared/src/consts/authConsts'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; +import { + EOnboardingPagesV2, + EOnboardingV2Routes, + ERootRoutes, +} from '@onekeyhq/shared/src/routes'; import { formatDate } from '@onekeyhq/shared/src/utils/dateUtils'; import stringUtils from '@onekeyhq/shared/src/utils/stringUtils'; @@ -58,6 +64,7 @@ function demoError(error: unknown, apiName: string) { } function OneKeyIDApiTests() { + const navigation = useAppNavigation(); const [email, setEmail] = useState(''); const [otp, setOtp] = useState(''); const [loading, setLoading] = useState(null); @@ -487,6 +494,24 @@ function OneKeyIDApiTests() { Sign Out + + + Onboarding Test + + + + ); } diff --git a/packages/kit/src/views/Onboarding/pages/FinalizeWalletSetup.tsx b/packages/kit/src/views/Onboarding/pages/FinalizeWalletSetup.tsx index 6b453388534a..07df1e3c9673 100644 --- a/packages/kit/src/views/Onboarding/pages/FinalizeWalletSetup.tsx +++ b/packages/kit/src/views/Onboarding/pages/FinalizeWalletSetup.tsx @@ -59,6 +59,7 @@ function FinalizeWalletSetupPage({ const mnemonic = route?.params?.mnemonic; const mnemonicType = route?.params?.mnemonicType; const isWalletBackedUp = route?.params?.isWalletBackedUp; + const isKeylessWallet = route?.params?.isKeylessWallet; const [onboardingError, setOnboardingError] = useState< IOneKeyError | undefined >(undefined); @@ -125,6 +126,7 @@ function FinalizeWalletSetupPage({ await actions.current.createHDWallet({ mnemonic, isWalletBackedUp, + isKeylessWallet, }); }, }); @@ -139,7 +141,15 @@ function FinalizeWalletSetupPage({ throw error; } })(); - }, [actions, intl, mnemonic, mnemonicType, popPage, isWalletBackedUp]); + }, [ + actions, + intl, + mnemonic, + mnemonicType, + popPage, + isWalletBackedUp, + isKeylessWallet, + ]); useEffect(() => { const fn = ( diff --git a/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx b/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx index de77c3f53f45..40fb42c25d22 100644 --- a/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx +++ b/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx @@ -15,6 +15,8 @@ import { } from '@onekeyhq/components'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; +import { KeylessOnboardingDebugPanel } from '../pages/KeylessOnboardingDebugPanel'; + import { OnboardingLayout } from './OnboardingLayout'; interface IPinInputLayoutProps { @@ -30,6 +32,7 @@ interface IPinInputLayoutProps { isSubmitDisabled?: boolean; isInputDisabled?: boolean; errorMessage?: string; + isLoading?: boolean; } function PinInputLayout({ @@ -45,6 +48,7 @@ function PinInputLayout({ isSubmitDisabled = false, isInputDisabled = false, errorMessage, + isLoading, }: IPinInputLayoutProps) { const inputRef = useRef(null); const { gtMd } = useMedia(); @@ -126,17 +130,20 @@ function PinInputLayout({ ) : null} ) : null} + + @@ -147,7 +154,8 @@ function PinInputLayout({ size="large" variant={isSubmitDisabled ? 'secondary' : 'primary'} onPress={onSubmit} - disabled={isSubmitDisabled} + loading={isLoading} + disabled={isSubmitDisabled || isLoading} > {buttonText} diff --git a/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx index 26251f65370b..c6f9a78394d1 100644 --- a/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/ConfirmPinPage.tsx @@ -3,53 +3,46 @@ import { useCallback, useEffect, useState } from 'react'; import { useRoute } from '@react-navigation/core'; import { useIntl } from 'react-intl'; -import { Dialog } from '@onekeyhq/components'; +import { Button, Dialog } from '@onekeyhq/components'; +import { EKeylessFinalizeAction } from '@onekeyhq/shared/src/keylessWallet/keylessWalletConsts'; import { ETranslations } from '@onekeyhq/shared/src/locale/enum/translations'; import type { IOnboardingParamListV2 } from '@onekeyhq/shared/src/routes'; import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; +import backgroundApiProxy from '../../../background/instance/backgroundApiProxy'; import { AccountSelectorProviderMirror } from '../../../components/AccountSelector/AccountSelectorProvider'; import { useKeylessWallet } from '../../../components/KeylessWallet/useKeylessWallet'; import useAppNavigation from '../../../hooks/useAppNavigation'; import { PinInputLayout } from '../components/PinInputLayout'; +import { KeylessOnboardingDebugPanel } from './KeylessOnboardingDebugPanel'; + function ConfirmPinPage() { const navigation = useAppNavigation(); - const { getKeylessOnboardingPin, cacheKeylessOnboardingPin } = - useKeylessWallet(); - - // Use state to store the PIN, fetched only once on mount - const [originalPin, setOriginalPin] = useState(undefined); + const { + confirmKeylessOnboardingPin, + getKeylessOnboardingPin, + handleKeylessOnboardingTimeout, + } = useKeylessWallet(); const intl = useIntl(); const [confirmPin, setConfirmPin] = useState(''); const [isValid, setIsValid] = useState(false); const [errorMessage, setErrorMessage] = useState(''); - // Fetch PIN only on mount (commit phase), not during render phase - // This ensures getAndDelete is called only once by the mounted instance - useEffect(() => { - const pin = getKeylessOnboardingPin(); - setOriginalPin(pin); - }, [getKeylessOnboardingPin]); - const handlePinChange = useCallback( - (filteredText: string) => { + async (filteredText: string) => { setConfirmPin(filteredText); setErrorMessage(''); - if (!originalPin) { - Dialog.show({ - icon: 'ErrorOutline', - tone: 'destructive', - title: 'Original PIN is not found. Please try again.', - }); - return; - } - // Auto-validate when 4 digits entered if (filteredText.length === 4) { + const originalPin = await getKeylessOnboardingPin({ skipDelete: true }); + if (!originalPin) { + handleKeylessOnboardingTimeout(); + return; + } if (filteredText === originalPin) { setIsValid(true); } else { @@ -62,14 +55,25 @@ function ConfirmPinPage() { setIsValid(false); } }, - [originalPin, intl], + [getKeylessOnboardingPin, handleKeylessOnboardingTimeout, intl], ); - const handleConfirm = useCallback(() => { + const handleConfirm = useCallback(async () => { setConfirmPin(''); - cacheKeylessOnboardingPin({ pin: originalPin || '' }); - navigation.push(EOnboardingPagesV2.CreatePasscode); - }, [cacheKeylessOnboardingPin, navigation, originalPin]); + const originalPin = await getKeylessOnboardingPin(); + if (!originalPin) { + handleKeylessOnboardingTimeout(); + return; + } + await confirmKeylessOnboardingPin({ + pin: originalPin || '', + action: EKeylessFinalizeAction.Create, + }); + }, [ + confirmKeylessOnboardingPin, + getKeylessOnboardingPin, + handleKeylessOnboardingTimeout, + ]); return ( { await enableKeylessWallet({ diff --git a/packages/kit/src/views/Onboardingv2/pages/CreatePasscodePage.tsx b/packages/kit/src/views/Onboardingv2/pages/CreatePasscodePage.tsx index 80406ce0785c..22ff8cc0fcac 100644 --- a/packages/kit/src/views/Onboardingv2/pages/CreatePasscodePage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/CreatePasscodePage.tsx @@ -5,14 +5,80 @@ import { useIntl } from 'react-intl'; import { Page, SizableText, Spinner, YStack } from '@onekeyhq/components'; import { usePasswordModeAtom } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; import { ETranslations } from '@onekeyhq/shared/src/locale'; +import type { + EOnboardingPagesV2, + IOnboardingParamListV2, +} from '@onekeyhq/shared/src/routes/onboardingv2'; +import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; import backgroundApiProxy from '../../../background/instance/backgroundApiProxy'; +import { AccountSelectorProviderMirror } from '../../../components/AccountSelector'; +import { useKeylessWallet } from '../../../components/KeylessWallet/useKeylessWallet'; import PasswordSetup from '../../../components/Password/components/PasswordSetup'; import PasswordSetupContainer from '../../../components/Password/container/PasswordSetupContainer'; +import PasswordVerifyContainer from '../../../components/Password/container/PasswordVerifyContainer'; +import { useAppRoute } from '../../../hooks/useAppRoute'; +import { usePromiseResult } from '../../../hooks/usePromiseResult'; import { OnboardingLayout } from '../components/OnboardingLayout'; import type { IPasswordSetupForm } from '../../../components/Password/components/PasswordSetup'; +function PasscodeFormView() { + const intl = useIntl(); + const { result: isPasswordSet } = usePromiseResult(async () => { + return backgroundApiProxy.servicePassword.checkPasswordSet(); + }, []); + const { finalizeKeylessWalletV2 } = useKeylessWallet(); + const route = useAppRoute< + IOnboardingParamListV2, + EOnboardingPagesV2.CreatePasscode + >(); + + const handlePasscodeConfirm = useCallback( + async (_passcode: string) => { + await finalizeKeylessWalletV2({ action: route?.params?.action }); + }, + [finalizeKeylessWalletV2, route.params.action], + ); + + if (isPasswordSet === undefined) { + return ; + } + let formView = null; + if (isPasswordSet) { + formView = ( + }> + + + ); + } else { + formView = ( + }> + + + ); + } + return ( + <> + + + {!isPasswordSet + ? intl.formatMessage({ + id: ETranslations.global_set_passcode, + }) + : intl.formatMessage({ + id: ETranslations.auth_confirm_passcode_form_label, + })} + + + {intl.formatMessage({ id: ETranslations.create_passcode_desc })} + + + {formView} + + ); +} + function CreatePasscodePage() { const intl = useIntl(); const [loading, setLoading] = useState(false); @@ -32,28 +98,7 @@ function CreatePasscodePage() { - - - {step === 'create' - ? intl.formatMessage({ - id: ETranslations.global_set_passcode, - }) - : intl.formatMessage({ - id: ETranslations.auth_confirm_passcode_form_label, - })} - - - {intl.formatMessage({ id: ETranslations.create_passcode_desc })} - - - }> - { - alert(data); - }} - /> - + @@ -61,4 +106,17 @@ function CreatePasscodePage() { ); } -export { CreatePasscodePage as default }; +function CreatePasscodePageWithContext() { + return ( + + + + ); +} + +export { CreatePasscodePageWithContext as default }; diff --git a/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx index 5dce623e758a..1d6a4d9feff8 100644 --- a/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/CreatePinPage.tsx @@ -26,9 +26,9 @@ function CreatePinPage() { const intl = useIntl(); const [pin, setPin] = useState(''); - const handleContinue = useCallback(() => { + const handleContinue = useCallback(async () => { if (pin) { - cacheKeylessOnboardingPin({ pin }); + await cacheKeylessOnboardingPin({ pin }); setPin(''); navigation.push(EOnboardingPagesV2.ConfirmPin); } diff --git a/packages/kit/src/views/Onboardingv2/pages/FinalizeWalletSetup.tsx b/packages/kit/src/views/Onboardingv2/pages/FinalizeWalletSetup.tsx index b2c5adf0a6ea..15da375f8c7e 100644 --- a/packages/kit/src/views/Onboardingv2/pages/FinalizeWalletSetup.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/FinalizeWalletSetup.tsx @@ -177,6 +177,7 @@ function FinalizeWalletSetupPage({ const deviceData = route?.params?.deviceData; const isFirmwareVerified = route?.params?.isFirmwareVerified; const isWalletBackedUp = route?.params?.isWalletBackedUp; + const isKeylessWallet = route?.params?.isKeylessWallet; const initialStep = EFinalizeWalletSetupSteps.CreatingWallet; @@ -304,6 +305,7 @@ function FinalizeWalletSetupPage({ await actions.current.createHDWallet({ mnemonic, isWalletBackedUp, + isKeylessWallet, }); }, }); @@ -344,6 +346,7 @@ function FinalizeWalletSetupPage({ mnemonicType, actions, isWalletBackedUp, + isKeylessWallet, goNextStep, connectDevice, createHWWallet, diff --git a/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx b/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx new file mode 100644 index 000000000000..ca939b4a2892 --- /dev/null +++ b/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx @@ -0,0 +1,93 @@ +import { useCallback } from 'react'; + +import { Button, Dialog, Input, Toast, YStack } from '@onekeyhq/components'; +import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; + +import backgroundApiProxy from '../../../background/instance/backgroundApiProxy'; +import { useKeylessWallet } from '../../../components/KeylessWallet/useKeylessWallet'; +import { MultipleClickStack } from '../../../components/MultipleClickStack'; +import useAppNavigation from '../../../hooks/useAppNavigation'; + +export function KeylessOnboardingDebugPanel() { + const navigation = useAppNavigation(); + const { cacheKeylessOnboardingCustomMnemonic } = useKeylessWallet(); + + const handleImportCustomMnemonic = useCallback(() => { + Dialog.confirm({ + title: 'Custom Mnemonic', + renderContent: ( + + + + + + ), + onConfirm: async (dialogInstance) => { + const form = dialogInstance.getForm(); + if (!form) { + return; + } + const { mnemonic } = form.getValues() as { mnemonic: string }; + if (!mnemonic?.trim()) { + return; + } + + await cacheKeylessOnboardingCustomMnemonic({ + customMnemonic: mnemonic.trim(), + }); + + Toast.success({ + title: 'Custom mnemonic saved.', + }); + }, + }); + }, [cacheKeylessOnboardingCustomMnemonic]); + + return ( + + + + + + + } + /> + + ); +} diff --git a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx index d1d7e90c2cb7..393d647f9759 100644 --- a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx @@ -16,13 +16,11 @@ import { import { EOAuthSocialLoginProvider } from '@onekeyhq/shared/src/consts/authConsts'; import { ETranslations } from '@onekeyhq/shared/src/locale/enum/translations'; import type { + EOnboardingPagesV2, EOnboardingV2OneKeyIDLoginMode, IOnboardingParamListV2, } from '@onekeyhq/shared/src/routes'; -import { - EOnboardingPagesV2, - IOnboardingParamList, -} from '@onekeyhq/shared/src/routes'; +import { IOnboardingParamList } from '@onekeyhq/shared/src/routes'; import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; import { AccountSelectorProviderMirror } from '../../../components/AccountSelector/AccountSelectorProvider'; @@ -121,27 +119,14 @@ function OneKeyIDLoginPage() { const intl = useIntl(); const { logout, signInWithSocialLogin } = useOneKeyAuth(); - const { - createOrRestoreKeylessWallet, - cacheKeylessOnboardingToken, - checkKeylessWalletCreatedOnServer, - } = useKeylessWallet(); + const { cacheKeylessOnboardingToken, checkKeylessWalletInitedOnServer } = + useKeylessWallet(); const goToInputPinPage = useCallback( async ({ token }: { token: string }) => { - const { isCreated } = await checkKeylessWalletCreatedOnServer({ token }); - await cacheKeylessOnboardingToken({ token }); - if (isCreated) { - navigation.push(EOnboardingPagesV2.VerifyPin); - } else { - navigation.push(EOnboardingPagesV2.CreatePin); - } + await checkKeylessWalletInitedOnServer({ token }); }, - [ - cacheKeylessOnboardingToken, - checkKeylessWalletCreatedOnServer, - navigation, - ], + [checkKeylessWalletInitedOnServer], ); const handleSocialLogin = useCallback( diff --git a/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx index 8a81269a666e..7e378bcd15a9 100644 --- a/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx @@ -3,13 +3,20 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useRoute } from '@react-navigation/core'; import { useIntl } from 'react-intl'; +import { Button } from '@onekeyhq/components'; import { ETranslations } from '@onekeyhq/shared/src/locale'; import type { IOnboardingParamListV2 } from '@onekeyhq/shared/src/routes'; import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; +import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; +import backgroundApiProxy from '../../../background/instance/backgroundApiProxy'; +import { AccountSelectorProviderMirror } from '../../../components/AccountSelector/AccountSelectorProvider'; +import { useKeylessWallet } from '../../../components/KeylessWallet/useKeylessWallet'; import useAppNavigation from '../../../hooks/useAppNavigation'; import { PinInputLayout } from '../components/PinInputLayout'; +import { KeylessOnboardingDebugPanel } from './KeylessOnboardingDebugPanel'; + import type { RouteProp } from '@react-navigation/core'; const MAX_ATTEMPTS = 7; @@ -31,6 +38,8 @@ function VerifyPinPage() { const route = useRoute>(); const { verifyType = 'socialLogin' } = route.params ?? {}; + const { verifyKeylessOnboardingPin } = useKeylessWallet(); + const [isLoading, setIsLoading] = useState(false); const isSocialLogin = verifyType === 'socialLogin'; @@ -108,7 +117,17 @@ function VerifyPinPage() { [isInputDisabled], ); - const handleVerify = useCallback(() => { + const handleVerify = useCallback(async () => { + try { + setIsLoading(true); + await verifyKeylessOnboardingPin({ pin }); + } finally { + setIsLoading(false); + setPin(''); + } + }, [verifyKeylessOnboardingPin, pin]); + + const handleVerifyLegacy = useCallback(() => { // TODO: Verify against actual stored PIN on server const isCorrect = false; // Mock: always fail for testing @@ -191,6 +210,7 @@ function VerifyPinPage() { return ( + + + ); +} + +export { VerifyPinPageWithContext as default }; diff --git a/packages/shared/src/keylessWallet/keylessWalletConsts.ts b/packages/shared/src/keylessWallet/keylessWalletConsts.ts index 0c07b95fc6d6..8e01c7927262 100644 --- a/packages/shared/src/keylessWallet/keylessWalletConsts.ts +++ b/packages/shared/src/keylessWallet/keylessWalletConsts.ts @@ -2,3 +2,7 @@ export enum EKeylessWalletEnableScene { Onboarding = 'onboarding', GetMnemonic = 'getMnemonic', } +export enum EKeylessFinalizeAction { + Create = 'create', + Restore = 'restore', +} diff --git a/packages/shared/src/keylessWallet/keylessWalletTypes.ts b/packages/shared/src/keylessWallet/keylessWalletTypes.ts index 126aa8717ab8..98c944b3221f 100644 --- a/packages/shared/src/keylessWallet/keylessWalletTypes.ts +++ b/packages/shared/src/keylessWallet/keylessWalletTypes.ts @@ -109,6 +109,12 @@ export type IKeylessBackendShare = { backendShare: string; }; +export type IKeylessJuiceboxShare = { + ownerId: string; + pin: string; + juiceboxShare: string; +}; + export type ISupabaseJWTPayload = JWTPayload & { app_metadata: { provider: string; diff --git a/packages/shared/src/routes/onboarding.ts b/packages/shared/src/routes/onboarding.ts index 42857742652a..44e1a2871a05 100644 --- a/packages/shared/src/routes/onboarding.ts +++ b/packages/shared/src/routes/onboarding.ts @@ -113,8 +113,9 @@ export type IOnboardingParamList = { // finalize wallet setup [EOnboardingPages.FinalizeWalletSetup]: { mnemonic?: string; - mnemonicType?: EMnemonicType; + mnemonicType?: EMnemonicType; // bip39 or ton isWalletBackedUp?: boolean; + isKeylessWallet?: boolean; }; // device management guide page diff --git a/packages/shared/src/routes/onboardingv2.ts b/packages/shared/src/routes/onboardingv2.ts index 1c7b363aed0c..831a11514544 100644 --- a/packages/shared/src/routes/onboardingv2.ts +++ b/packages/shared/src/routes/onboardingv2.ts @@ -1,5 +1,6 @@ import type { EConnectDeviceChannel } from '../../types/connectDevice'; import type { IConnectYourDeviceItem } from '../../types/device'; +import { EKeylessFinalizeAction } from '../keylessWallet/keylessWalletConsts'; import type { IDetectedNetworkGroupItem } from '../utils/networkDetectUtils'; import type { EMnemonicType } from '../utils/secret'; import type { EDeviceType } from '@onekeyfe/hd-shared'; @@ -20,8 +21,7 @@ export enum EOnboardingV2KeylessWalletCreationMode { } export enum EOnboardingV2OneKeyIDLoginMode { - CreateKeylessWallet = 'CreateKeylessWallet', - ImportKeylessWallet = 'ImportKeylessWallet', + CreateOrImportKeylessWallet = 'CreateOrImportKeylessWallet', VerifyKeylessWallet = 'VerifyKeylessWallet', } @@ -75,6 +75,7 @@ export type IOnboardingParamListV2 = { mnemonic?: string; mnemonicType?: EMnemonicType; isWalletBackedUp?: boolean; + isKeylessWallet?: boolean; isFirmwareVerified?: boolean; deviceData?: IConnectYourDeviceItem; keylessPackSetId?: string; @@ -132,7 +133,9 @@ export type IOnboardingParamListV2 = { isResetPin?: boolean; }; [EOnboardingPagesV2.ConfirmPin]: undefined; - [EOnboardingPagesV2.CreatePasscode]: undefined; + [EOnboardingPagesV2.CreatePasscode]: { + action: EKeylessFinalizeAction; + }; [EOnboardingPagesV2.VerifyPin]: { /** * 'socialLogin' - User verifies PIN after social login (with retry mechanism) From bb501784853e7dbcc74d5835c468375740d2e7eb Mon Sep 17 00:00:00 2001 From: morizon Date: Sat, 27 Dec 2025 22:44:44 +0800 Subject: [PATCH 32/66] feat: refactor PinInputLayout to support forward ref and enhance focus handling; update KeylessOnboardingDebugPanel button text; clean up unused code in OneKeyIDLoginPage and VerifyPinPage --- .../ServiceKeylessWallet.ts | 50 +-- .../components/PinInputLayout.tsx | 307 ++++++++++-------- .../pages/KeylessOnboardingDebugPanel.tsx | 2 +- .../Onboardingv2/pages/OneKeyIDLoginPage.tsx | 3 +- .../Onboardingv2/pages/VerifyPinPage.tsx | 96 ++++-- packages/shared/src/routes/onboardingv2.ts | 2 +- 6 files changed, 263 insertions(+), 197 deletions(-) diff --git a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts index ecc19b1e33b3..705701d6889e 100644 --- a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts +++ b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts @@ -1159,8 +1159,10 @@ class ServiceKeylessWallet extends ServiceBase { @toastIfError() async apiGetKeylessJuiceboxShare(params: { token: string; + pin: string; }): Promise { - const { token } = params; + const { token, pin } = params; + await this.apiVerifyKeylessJuiceboxPin({ token, pin }); const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); // TODO: Replace with real API call // exchange juicebox token from onekey auth server @@ -1170,6 +1172,22 @@ class ServiceKeylessWallet extends ServiceBase { return mockedShares[ownerId]?.juiceboxShare || null; } + // apiVerifyKeylessJuiceboxPin + @backgroundMethod() + @toastIfError() + async apiVerifyKeylessJuiceboxPin(params: { token: string; pin: string }) { + await timerUtils.wait(1500); + const { token, pin } = params; + const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); + // TODO: Replace with real API call + // For now, verify PIN from mock cache + const mockedShares = await this.getMockedKeylessShares(); + const juiceboxShare = mockedShares[ownerId]?.juiceboxShare; + if (!juiceboxShare || juiceboxShare?.pin !== pin) { + throw new OneKeyLocalError('Invalid PIN'); + } + } + @backgroundMethod() @toastIfError() async apiUploadKeylessBackendShare(params: { @@ -1228,22 +1246,6 @@ class ServiceKeylessWallet extends ServiceBase { return juiceboxShareData; } - // apiVerifyKeylessJuiceboxPin - @backgroundMethod() - @toastIfError() - async apiVerifyKeylessJuiceboxPin(params: { token: string; pin: string }) { - await timerUtils.wait(1500); - const { token, pin } = params; - const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); - // TODO: Replace with real API call - // For now, verify PIN from mock cache - const mockedShares = await this.getMockedKeylessShares(); - const juiceboxShare = mockedShares[ownerId]?.juiceboxShare; - if (!juiceboxShare || juiceboxShare?.pin !== pin) { - throw new OneKeyLocalError('Invalid PIN'); - } - } - @backgroundMethod() @toastIfError() async restoreKeylessWalletFromServer(params: { @@ -1264,18 +1266,20 @@ class ServiceKeylessWallet extends ServiceBase { throw new OneKeyLocalError('Keyless wallet not initialized'); } - // Verify PIN - await this.apiVerifyKeylessJuiceboxPin({ token, pin }); + // Get juicebox share from juicebox network + const juiceboxShareData = await this.apiGetKeylessJuiceboxShare({ + token, + pin, + }); + if (!juiceboxShareData) { + throw new OneKeyLocalError('Juicebox share not found'); + } // Get shares from server const backendShareData = await this.apiGetKeylessBackendShare({ token }); if (!backendShareData) { throw new OneKeyLocalError('Backend share not found'); } - const juiceboxShareData = await this.apiGetKeylessJuiceboxShare({ token }); - if (!juiceboxShareData) { - throw new OneKeyLocalError('Juicebox share not found'); - } // Combine shares to recover mnemonic password const mnemonicPasswordShares = [ diff --git a/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx b/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx index 40fb42c25d22..52040febdef9 100644 --- a/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx +++ b/packages/kit/src/views/Onboardingv2/components/PinInputLayout.tsx @@ -1,4 +1,5 @@ -import { useCallback, useRef } from 'react'; +import type { ForwardRefExoticComponent, RefAttributes } from 'react'; +import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; import { useFocusEffect } from '@react-navigation/core'; import { KeyboardAvoidingView, type TextInput } from 'react-native'; @@ -35,158 +36,178 @@ interface IPinInputLayoutProps { isLoading?: boolean; } -function PinInputLayout({ - title, - description, - descriptionColor = '$textSubdued', - buttonText, - secondaryButtonText, - onSecondaryButtonPress, - value, - onChange, - onSubmit, - isSubmitDisabled = false, - isInputDisabled = false, - errorMessage, - isLoading, -}: IPinInputLayoutProps) { - const inputRef = useRef(null); - const { gtMd } = useMedia(); - - useFocusEffect( - useCallback(() => { - const timer = setTimeout( - () => { - inputRef.current?.focus(); - }, - platformEnv.isNative ? 600 : 300, - ); - return () => clearTimeout(timer); - }, []), - ); - - const handleChangeText = useCallback( - (text: string) => { - onChange(text.replace(/[^0-9]/g, '')); +export interface IPinInputLayoutRef { + focus: () => void; +} + +const PinInputLayout = forwardRef( + ( + { + title, + description, + descriptionColor = '$textSubdued', + buttonText, + secondaryButtonText, + onSecondaryButtonPress, + value, + onChange, + onSubmit, + isSubmitDisabled = false, + isInputDisabled = false, + errorMessage, + isLoading, }, - [onChange], - ); - - const handleSubmitEditing = useCallback(() => { - if (!isSubmitDisabled) { - onSubmit(); - } - }, [isSubmitDisabled, onSubmit]); - - const content = ( - - - - - - {title} - - {description} - - - - - - - - {errorMessage ? ( - - {errorMessage} - - ) : null} - - - {gtMd ? ( - - {secondaryButtonText && onSecondaryButtonPress ? ( + ref, + ) => { + const inputRef = useRef(null); + const { gtMd } = useMedia(); + + useImperativeHandle(ref, () => ({ + focus: () => { + inputRef.current?.focus(); + }, + })); + + useFocusEffect( + useCallback(() => { + const timer = setTimeout( + () => { + inputRef.current?.focus(); + }, + platformEnv.isNative ? 600 : 300, + ); + return () => clearTimeout(timer); + }, []), + ); + + const handleChangeText = useCallback( + (text: string) => { + onChange(text.replace(/[^0-9]/g, '')); + }, + [onChange], + ); + + const handleSubmitEditing = useCallback(() => { + if (!isSubmitDisabled) { + onSubmit(); + } + }, [isSubmitDisabled, onSubmit]); + + const content = ( + + + + + + {title} + + {description} + + + + + + + + {errorMessage ? ( + + {errorMessage} + + ) : null} + + + {gtMd ? ( + + {secondaryButtonText && onSecondaryButtonPress ? ( + + ) : null} - ) : null} - - - ) : null} - - - - - - {!gtMd ? ( - - - - {secondaryButtonText && onSecondaryButtonPress ? ( + + ) : null} + + + + + + {!gtMd ? ( + + - ) : null} - - - ) : null} - - ); - - return ( - - {platformEnv.isNative ? ( - - {content} - - ) : ( - content - )} - - ); -} + {secondaryButtonText && onSecondaryButtonPress ? ( + + ) : null} + + + ) : null} + + ); + + return ( + + {platformEnv.isNative ? ( + + {content} + + ) : ( + content + )} + + ); + }, +); + +PinInputLayout.displayName = 'PinInputLayout'; export { PinInputLayout }; +export type IPinInputLayoutComponent = ForwardRefExoticComponent< + IPinInputLayoutProps & RefAttributes +>; diff --git a/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx b/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx index ca939b4a2892..a4d029ac4082 100644 --- a/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx @@ -80,7 +80,7 @@ export function KeylessOnboardingDebugPanel() { void backgroundApiProxy.serviceKeylessWallet.clearMockedKeylessShares(); }} > - 删除服务器钱包数据 + 重置云端钱包 + ) : null} - ) : null} + + ) : null} + + + + + + {!gtMd ? ( + + + - - ) : null} - - - - - - {!gtMd ? ( - - - - {secondaryButtonText && onSecondaryButtonPress ? ( - - ) : null} - - - ) : null} - - ); - - return ( - - {platformEnv.isNative ? ( - - {content} - - ) : ( - content - )} + {secondaryButtonText && onSecondaryButtonPress ? ( + + ) : null} + + + + ) : null} + ); }, diff --git a/packages/kit/src/views/Onboardingv2/components/PinInputLayout2.tsx b/packages/kit/src/views/Onboardingv2/components/PinInputLayout2.tsx deleted file mode 100644 index a81462d74a8a..000000000000 --- a/packages/kit/src/views/Onboardingv2/components/PinInputLayout2.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { useCallback, useMemo, useRef } from 'react'; - -import { useFocusEffect } from '@react-navigation/core'; -import { type TextInput } from 'react-native'; - -import { - Button, - HeightTransition, - Input, - Keyboard, - Page, - SizableText, - XStack, - YStack, - useMedia, -} from '@onekeyhq/components'; -import platformEnv from '@onekeyhq/shared/src/platformEnv'; - -import { OnboardingLayout } from './OnboardingLayout'; - -interface IPinInputLayoutProps { - title: string; - description?: string | React.ReactNode; - descriptionColor?: '$textSubdued' | '$textCaution'; - buttonText: string; - secondaryButtonText?: string; - onSecondaryButtonPress?: () => void; - value: string; - onChange: (pin: string) => void; - onSubmit: () => void; - isSubmitDisabled?: boolean; - isInputDisabled?: boolean; - errorMessage?: string; - placeholder?: string; -} - -function PinInputLayout({ - title, - description, - descriptionColor = '$textSubdued', - buttonText, - secondaryButtonText, - onSecondaryButtonPress, - value, - onChange, - onSubmit, - isSubmitDisabled = false, - isInputDisabled = false, - errorMessage, - placeholder = '••••', -}: IPinInputLayoutProps) { - const inputRef = useRef(null); - const { gtMd } = useMedia(); - - useFocusEffect( - useCallback(() => { - const timer = setTimeout( - () => { - inputRef.current?.focus(); - }, - platformEnv.isNative ? 500 : 300, - ); - return () => clearTimeout(timer); - }, []), - ); - - const handleChangeText = useCallback( - (text: string) => { - onChange(text.replace(/[^0-9]/g, '')); - }, - [onChange], - ); - - const handleSubmitEditing = useCallback(() => { - if (!isSubmitDisabled) { - onSubmit(); - } - }, [isSubmitDisabled, onSubmit]); - - return ( - - - - - - - {title} - - {description} - - - - - - - - {errorMessage ? ( - - {errorMessage} - - ) : null} - - - {gtMd ? ( - - {secondaryButtonText && onSecondaryButtonPress ? ( - - ) : null} - - - ) : null} - - - - {!gtMd ? ( - - - - - {secondaryButtonText && onSecondaryButtonPress ? ( - - ) : null} - - - - ) : null} - - - ); -} - -export { PinInputLayout }; diff --git a/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx b/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx index a4d029ac4082..e5448f6a9d43 100644 --- a/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx @@ -61,13 +61,13 @@ export function KeylessOnboardingDebugPanel() { }, [cacheKeylessOnboardingCustomMnemonic]); return ( - + + diff --git a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx index 3559ea818ea7..5b163adb6826 100644 --- a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx @@ -17,10 +17,9 @@ import { EOAuthSocialLoginProvider } from '@onekeyhq/shared/src/consts/authConst import { ETranslations } from '@onekeyhq/shared/src/locale/enum/translations'; import type { EOnboardingPagesV2, - EOnboardingV2OneKeyIDLoginMode, IOnboardingParamListV2, } from '@onekeyhq/shared/src/routes'; -import { IOnboardingParamList } from '@onekeyhq/shared/src/routes'; +import { EOnboardingV2OneKeyIDLoginMode } from '@onekeyhq/shared/src/routes'; import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; import { AccountSelectorProviderMirror } from '../../../components/AccountSelector/AccountSelectorProvider'; @@ -31,6 +30,8 @@ import useAppNavigation from '../../../hooks/useAppNavigation'; import { useAppRoute } from '../../../hooks/useAppRoute'; import { OnboardingLayout } from '../components/OnboardingLayout'; +import { KeylessOnboardingDebugPanel } from './KeylessOnboardingDebugPanel'; + function OptionItem({ icon, iconProps, @@ -121,13 +122,25 @@ function OneKeyIDLoginPage() { const intl = useIntl(); const { logout, signInWithSocialLogin } = useOneKeyAuth(); - const { checkKeylessWalletInitedOnServer } = useKeylessWallet(); + const { + checkKeylessWalletInitedOnServer, + checkKeylessWalletInitedOnServerForResetPin, + } = useKeylessWallet(); const goToInputPinPage = useCallback( async ({ token }: { token: string }) => { - await checkKeylessWalletInitedOnServer({ token }); + // For Reset PIN mode, navigate directly to CreatePin with ResetPin action + if (mode === EOnboardingV2OneKeyIDLoginMode.ResetPin) { + await checkKeylessWalletInitedOnServerForResetPin({ token }); + } else { + await checkKeylessWalletInitedOnServer({ token }); + } }, - [checkKeylessWalletInitedOnServer], + [ + checkKeylessWalletInitedOnServer, + checkKeylessWalletInitedOnServerForResetPin, + mode, + ], ); const handleSocialLogin = useCallback( @@ -191,6 +204,7 @@ function OneKeyIDLoginPage() { onPress={handleAppleLogin} /> + diff --git a/packages/kit/src/views/Setting/pages/Tab/config.tsx b/packages/kit/src/views/Setting/pages/Tab/config.tsx index a390592896e6..c78eff188e83 100644 --- a/packages/kit/src/views/Setting/pages/Tab/config.tsx +++ b/packages/kit/src/views/Setting/pages/Tab/config.tsx @@ -12,6 +12,7 @@ import type { import { Dialog } from '@onekeyhq/components'; import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; import { + useKeylessWallet, useKeylessWalletExistsLocal, useKeylessWalletFeatureIsEnabled, } from '@onekeyhq/kit/src/components/KeylessWallet/useKeylessWallet'; @@ -51,6 +52,7 @@ import { EModalRoutes, EModalSettingRoutes, EOnboardingPagesV2, + EOnboardingV2OneKeyIDLoginMode, EOnboardingV2Routes, ERootRoutes, } from '@onekeyhq/shared/src/routes'; @@ -166,6 +168,7 @@ export const useSettingsConfig: () => ISettingsConfig = () => { const isKeylessWalletEnabled = useKeylessWalletFeatureIsEnabled(); const isKeylessWalletExistsLocal = useKeylessWalletExistsLocal(); + const { goToOneKeyIDLoginPageForKeylessWallet } = useKeylessWallet(); return useMemo( () => [ @@ -522,11 +525,8 @@ export const useSettingsConfig: () => ISettingsConfig = () => { icon: 'InputOutline', title: intl.formatMessage({ id: ETranslations.reset_pin }), onPress: (navigation) => { - navigation?.navigate(ERootRoutes.Onboarding, { - screen: EOnboardingV2Routes.OnboardingV2, - params: { - screen: EOnboardingPagesV2.NewPinCreated, - }, + goToOneKeyIDLoginPageForKeylessWallet({ + mode: EOnboardingV2OneKeyIDLoginMode.ResetPin, }); }, }, @@ -842,6 +842,7 @@ export const useSettingsConfig: () => ISettingsConfig = () => { ], [ intl, + goToOneKeyIDLoginPageForKeylessWallet, cloudBackupFeatureInfo?.supportCloudBackup, cloudBackupFeatureInfo?.icon, cloudBackupFeatureInfo?.title, diff --git a/packages/kit/src/views/Setting/pages/Tab/index.tsx b/packages/kit/src/views/Setting/pages/Tab/index.tsx index 88f7da665913..d55b492ebcca 100644 --- a/packages/kit/src/views/Setting/pages/Tab/index.tsx +++ b/packages/kit/src/views/Setting/pages/Tab/index.tsx @@ -20,8 +20,10 @@ import { useSafeAreaInsets, } from '@onekeyhq/components'; import { DesktopTabItem } from '@onekeyhq/components/src/layouts/Navigation/Tab/TabBar/DesktopTabItem'; +import { AccountSelectorProviderMirror } from '@onekeyhq/kit/src/components/AccountSelector'; import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; +import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; import { ESettingsTabNames, useSettingsConfig } from './config'; import { ConfigContext } from './configContext'; @@ -246,7 +248,16 @@ function SettingTab() { }); } }, [appNavigation, isTabNavigator]); - return isTabNavigator ? : ; + return ( + + {isTabNavigator ? : } + + ); } export default memo(SettingTab); diff --git a/packages/shared/src/keylessWallet/keylessWalletConsts.ts b/packages/shared/src/keylessWallet/keylessWalletConsts.ts index 8e01c7927262..975d618a5da8 100644 --- a/packages/shared/src/keylessWallet/keylessWalletConsts.ts +++ b/packages/shared/src/keylessWallet/keylessWalletConsts.ts @@ -5,4 +5,5 @@ export enum EKeylessWalletEnableScene { export enum EKeylessFinalizeAction { Create = 'create', Restore = 'restore', + ResetPin = 'resetPin', } diff --git a/packages/shared/src/keylessWallet/shamirUtils.ts b/packages/shared/src/keylessWallet/shamirUtils.ts index a52a8adb4192..9ef6ae075cda 100644 --- a/packages/shared/src/keylessWallet/shamirUtils.ts +++ b/packages/shared/src/keylessWallet/shamirUtils.ts @@ -134,9 +134,65 @@ function recoverMissingShare(params: { return bufferUtils.bytesToBase64(missingShareBytes); } +/** + * Recover the missing Shamir share using base64-encoded secret. + * This is used for Reset PIN flow where the secret is mnemonicPassword (base64). + * + * @param secretBase64 - The secret (mnemonicPassword) as base64 string + * @param shareBase64 - One of the existing shares (backendShare) as base64 string + * @param missingX - The x-coordinate of the missing share (juiceboxShare) + * @returns The recovered share as base64 string + */ +function recoverMissingShareFromSecret(params: { + secretBase64: string; + shareBase64: string; + missingX: number; +}): string { + const { secretBase64, shareBase64, missingX } = params; + const shareBytes = bufferUtils.base64ToBytes(shareBase64); + const secretBytes = bufferUtils.base64ToBytes(secretBase64); + + // Share format: [y-values (N bytes), x-coordinate (1 byte)] + // x-coordinate is the LAST byte + const secretLength = shareBytes.length - 1; + const x1 = shareBytes[secretLength]; // x-coordinate at the end + const y1 = shareBytes.slice(0, secretLength); // y-values at the beginning + + // Verify secret length matches share length + if (secretBytes.length !== secretLength) { + throw new OneKeyLocalError( + `Secret length (${secretBytes.length}) does not match share length (${secretLength})`, + ); + } + + // Compute y-values for the missing share + const missingY = new Uint8Array(secretLength); + + for (let i = 0; i < secretLength; i += 1) { + const s = secretBytes[i]; // secret byte + const yi = y1[i]; // share1's y-value for this byte + + // a1 = (y1 - s) / x1 in GF(256) + // In GF(256), subtraction is XOR + const a1 = GF256.div(GF256.sub(yi, s), x1); + + // missingY = s + a1 * missingX + missingY[i] = GF256.add(s, GF256.mul(a1, missingX)); + } + + // Construct the missing share: [y-values, x-coordinate] + // Same format as shamir-secret-sharing library + const missingShareBytes = new Uint8Array(secretLength + 1); + missingShareBytes.set(missingY, 0); + missingShareBytes[secretLength] = missingX; + + return bufferUtils.bytesToBase64(missingShareBytes); +} + export default { GF256, split, combine, recoverMissingShare, + recoverMissingShareFromSecret, }; diff --git a/packages/shared/src/routes/onboardingv2.ts b/packages/shared/src/routes/onboardingv2.ts index d2a0f04411a5..4b96316d3e32 100644 --- a/packages/shared/src/routes/onboardingv2.ts +++ b/packages/shared/src/routes/onboardingv2.ts @@ -23,6 +23,7 @@ export enum EOnboardingV2KeylessWalletCreationMode { export enum EOnboardingV2OneKeyIDLoginMode { CreateOrImportKeylessWallet = 'CreateOrImportKeylessWallet', VerifyKeylessWallet = 'VerifyKeylessWallet', + ResetPin = 'ResetPin', } export enum EOnboardingPagesV2 { @@ -130,9 +131,11 @@ export type IOnboardingParamListV2 = { mode: EOnboardingV2OneKeyIDLoginMode; }; [EOnboardingPagesV2.CreatePin]: { - isResetPin?: boolean; + action?: EKeylessFinalizeAction; + }; + [EOnboardingPagesV2.ConfirmPin]: { + action?: EKeylessFinalizeAction; }; - [EOnboardingPagesV2.ConfirmPin]: undefined; [EOnboardingPagesV2.CreatePasscode]: { action: EKeylessFinalizeAction; }; diff --git a/packages/shared/src/utils/accountUtils.ts b/packages/shared/src/utils/accountUtils.ts index d40e4dfb700e..f1975ad84364 100644 --- a/packages/shared/src/utils/accountUtils.ts +++ b/packages/shared/src/utils/accountUtils.ts @@ -1015,12 +1015,21 @@ function buildKeylessDevicePackKey({ return `OneKey_Keyless__${packSetId}`; } +function buildKeylessMnemonicPasswordKey({ + ownerId, +}: { + ownerId: string; +}): string { + return `OneKey_Keyless_MnemonicPwd__${ownerId}`; +} + export default { URL_ACCOUNT_ID, HYPERLIQUID_AGENT_CREDENTIAL_PREFIX, getKeylessWalletPackSetId, buildKeylessDevicePackKey, + buildKeylessMnemonicPasswordKey, buildKeylessWalletId, buildAccountValueKey, parseAccountValueKey, From 135da8bce2002c64fed014aee4f1e297b5f4fc72 Mon Sep 17 00:00:00 2001 From: morizon Date: Mon, 29 Dec 2025 19:37:22 +0800 Subject: [PATCH 37/66] feat: enhance ServiceKeylessWallet to include juiceboxShareX and backendShareX for improved share recovery; update related types and logic for handling shares --- .../ServiceKeylessWallet.ts | 48 +++++++++++-------- .../src/keylessWallet/keylessWalletTypes.ts | 2 + 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts index 732b62186dae..9e73373e608f 100644 --- a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts +++ b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts @@ -1218,9 +1218,10 @@ class ServiceKeylessWallet extends ServiceBase { token: string; encryptedMnemonic: string; backendShare: string; + juiceboxShareX: number; }): Promise { await timerUtils.wait(1500, { devOnly: true }); - const { token, encryptedMnemonic, backendShare } = params; + const { token, encryptedMnemonic, backendShare, juiceboxShareX } = params; const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); // TODO: Replace with real API call // For now, save to mock cache @@ -1228,6 +1229,7 @@ class ServiceKeylessWallet extends ServiceBase { ownerId, encryptedMnemonic, backendShare, + juiceboxShareX, }; const mockedShares = await this.getMockedKeylessShares(); if (!mockedShares[ownerId]) { @@ -1247,9 +1249,10 @@ class ServiceKeylessWallet extends ServiceBase { token: string; pin: string; juiceboxShare: string; + backendShareX: number; }): Promise { await timerUtils.wait(1500, { devOnly: true }); - const { token, pin, juiceboxShare } = params; + const { token, pin, juiceboxShare, backendShareX } = params; const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); // TODO: Replace with real API call // exchange juicebox token from onekey auth server @@ -1259,6 +1262,7 @@ class ServiceKeylessWallet extends ServiceBase { ownerId, pin, juiceboxShare, + backendShareX, }; const mockedShares = await this.getMockedKeylessShares(); if (!mockedShares[ownerId]) { @@ -1285,6 +1289,8 @@ class ServiceKeylessWallet extends ServiceBase { if (!pin) { throw new OneKeyLocalError('pin is required'); } + + // check if keyless wallet is initialized const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); const mockedShares = await this.getMockedKeylessShares(); const existingShares = mockedShares[ownerId]; @@ -1292,6 +1298,12 @@ class ServiceKeylessWallet extends ServiceBase { throw new OneKeyLocalError('Keyless wallet not initialized'); } + // Get backend share from server + const backendShareData = await this.apiGetKeylessBackendShare({ token }); + if (!backendShareData) { + throw new OneKeyLocalError('Backend share not found'); + } + // Get juicebox share from juicebox network const juiceboxShareData = await this.apiGetKeylessJuiceboxShare({ token, @@ -1301,12 +1313,6 @@ class ServiceKeylessWallet extends ServiceBase { throw new OneKeyLocalError('Juicebox share not found'); } - // Get shares from server - const backendShareData = await this.apiGetKeylessBackendShare({ token }); - if (!backendShareData) { - throw new OneKeyLocalError('Backend share not found'); - } - // Combine shares to recover mnemonic password const mnemonicPasswordShares = [ bufferUtils.base64ToBytes(backendShareData.backendShare), @@ -1385,22 +1391,18 @@ class ServiceKeylessWallet extends ServiceBase { ); } - // 4. Extract x-coordinates from backendShare - // In 2-of-2 split, shares have x-coordinates 1 and 2 - // backendShare is share1 (x=1), juiceboxShare is share2 (x=2) - const backendShareBytes = bufferUtils.base64ToBytes( + // 4. Get x-coordinates from stored data + // juiceboxShareX is stored in backendShareData for recovery + const { juiceboxShareX } = backendShareData; + const backendShareX = keylessWalletUtils.getShareXCoordinate( backendShareData.backendShare, ); - const backendX = backendShareBytes[backendShareBytes.length - 1]; - // juiceboxX is the other share's x-coordinate - // For 2-of-2 split with x-coordinates [1, 2], if backendX is 1, juiceboxX is 2 - const juiceboxX = backendX === 1 ? 2 : 1; // 5. Recover juiceboxShare using recoverMissingShareFromSecret const juiceboxShare = await this.recoverMissingShareFromSecret({ secretBase64: mnemonicPassword, shareBase64: backendShareData.backendShare, - missingX: juiceboxX, + missingX: juiceboxShareX, }); // 6. Upload juiceboxShare with new PIN @@ -1408,6 +1410,7 @@ class ServiceKeylessWallet extends ServiceBase { token, juiceboxShare, pin: newPin, + backendShareX, }); return { success: true }; @@ -1461,6 +1464,11 @@ class ServiceKeylessWallet extends ServiceBase { mnemonicPasswordShare2, ); + // Extract x-coordinates from shares + const backendShareX = keylessWalletUtils.getShareXCoordinate(backendShare); + const juiceboxShareX = + keylessWalletUtils.getShareXCoordinate(juiceboxShare); + // Save mnemonicPassword to secure storage for Reset PIN flow await keylessMnemonicPasswordStorage.saveMnemonicPasswordToStorage({ ownerId, @@ -1468,18 +1476,20 @@ class ServiceKeylessWallet extends ServiceBase { backgroundApi: this.backgroundApi, }); - const backendShareData: IKeylessBackendShare = + const _backendShareData: IKeylessBackendShare = await this.apiUploadKeylessBackendShare({ token, encryptedMnemonic, backendShare, + juiceboxShareX, // Store the other share's x-coordinate for recovery }); // TODO verify backendShareData is valid - const juiceboxShareData: IKeylessJuiceboxShare = + const _juiceboxShareData: IKeylessJuiceboxShare = await this.apiUploadKeylessJuiceboxShare({ token, juiceboxShare, pin, + backendShareX, // Store the other share's x-coordinate for recovery }); // TODO verify juiceboxShareData is valid diff --git a/packages/shared/src/keylessWallet/keylessWalletTypes.ts b/packages/shared/src/keylessWallet/keylessWalletTypes.ts index 98c944b3221f..61c7e926b339 100644 --- a/packages/shared/src/keylessWallet/keylessWalletTypes.ts +++ b/packages/shared/src/keylessWallet/keylessWalletTypes.ts @@ -107,12 +107,14 @@ export type IKeylessBackendShare = { ownerId: string; encryptedMnemonic: string; backendShare: string; + juiceboxShareX: number; // x-coordinate of the juicebox share for recovery }; export type IKeylessJuiceboxShare = { ownerId: string; pin: string; juiceboxShare: string; + backendShareX: number; // x-coordinate of the backend share for recovery }; export type ISupabaseJWTPayload = JWTPayload & { From b9dace97355373cc5b5c61d5596362da7ceb4861 Mon Sep 17 00:00:00 2001 From: morizon Date: Mon, 29 Dec 2025 20:34:57 +0800 Subject: [PATCH 38/66] feat: update Keyless Wallet onboarding flow with new modes for Reset PIN and Verify PIN; refactor related components for improved navigation and state handling --- .gitignore | 1 + .../KeylessWallet/useKeylessWallet.tsx | 80 ++++++++++++++----- .../WalletEdit/WalletEditButton.tsx | 40 ++++++---- .../Onboardingv2/pages/OneKeyIDLoginPage.tsx | 18 +---- .../Onboardingv2/pages/VerifyPinPage.tsx | 16 ++-- .../src/views/Setting/pages/Tab/config.tsx | 2 +- packages/shared/src/routes/onboardingv2.ts | 12 +-- 7 files changed, 102 insertions(+), 67 deletions(-) diff --git a/.gitignore b/.gitignore index ea93a8e1f091..278d00a93752 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ __generated__ .clauderc .tmp/ *.private_ai_docs.md +.cursor/plans/ dist build-electron .next diff --git a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx index 4f65e949e301..7a13e5237618 100644 --- a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx +++ b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx @@ -568,7 +568,7 @@ export function useKeylessWallet() { }); } else { goToOneKeyIDLoginPageForKeylessWallet({ - mode: EOnboardingV2OneKeyIDLoginMode.CreateOrImportKeylessWallet, + mode: EOnboardingV2OneKeyIDLoginMode.KeylessCreateOrRestore, }); } } finally { @@ -578,11 +578,36 @@ export function useKeylessWallet() { }, [goToOneKeyIDLoginPageForKeylessWallet, intl]); const checkKeylessWalletInitedOnServer = useCallback( - async ({ token }: { token: string }) => { + async ({ + token, + mode, + }: { + token: string; + mode?: EOnboardingV2OneKeyIDLoginMode; + }) => { if (!token) { handleKeylessOnboardingTimeout(); return; } + await cacheKeylessOnboardingToken({ token }); + + // ResetPin: skip check, go to CreatePin + if (mode === EOnboardingV2OneKeyIDLoginMode.KeylessResetPin) { + navigation.push(EOnboardingPagesV2.CreatePin, { + action: EKeylessFinalizeAction.ResetPin, + }); + return; + } + + // VerifyPinOnly: skip check, go to VerifyPin + if (mode === EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly) { + navigation.push(EOnboardingPagesV2.VerifyPin, { + mode: EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly, + }); + return; + } + + // Default: check wallet existence and navigate accordingly const backendShareInfo = await backgroundApiProxy.serviceKeylessWallet.apiGetKeylessBackendShare( { @@ -590,7 +615,6 @@ export function useKeylessWallet() { }, ); const isInited = !!backendShareInfo; - await cacheKeylessOnboardingToken({ token }); if (isInited) { navigation.push(EOnboardingPagesV2.VerifyPin); } else { @@ -600,21 +624,6 @@ export function useKeylessWallet() { [handleKeylessOnboardingTimeout, navigation], ); - // For Reset PIN flow: navigate directly to CreatePin with action=ResetPin - const checkKeylessWalletInitedOnServerForResetPin = useCallback( - async ({ token }: { token: string }) => { - if (!token) { - handleKeylessOnboardingTimeout(); - return; - } - await cacheKeylessOnboardingToken({ token }); - navigation.push(EOnboardingPagesV2.CreatePin, { - action: EKeylessFinalizeAction.ResetPin, - }); - }, - [handleKeylessOnboardingTimeout, navigation], - ); - const finalizeKeylessWalletV2 = useCallback( async ({ action }: { action: EKeylessFinalizeAction }) => { const token = await getKeylessOnboardingToken(); @@ -700,7 +709,13 @@ export function useKeylessWallet() { ); const verifyKeylessOnboardingPin = useCallback( - async ({ pin }: { pin: string }) => { + async ({ + pin, + mode, + }: { + pin: string; + mode?: EOnboardingV2OneKeyIDLoginMode; + }) => { const token = await getKeylessOnboardingToken({ skipDelete: true }); if (!token) { handleKeylessOnboardingTimeout(); @@ -712,13 +727,35 @@ export function useKeylessWallet() { pin, }, ); + + // VerifyPinOnly: just verify, show success dialog and close modal + if (mode === EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly) { + Dialog.show({ + title: 'PIN Verified', + showCancelButton: false, + onConfirmText: intl.formatMessage({ + id: ETranslations.global_got_it, + }), + onConfirm: () => { + navigation.popStack(); + }, + }); + return; + } + + // Default: continue with restore flow await cacheKeylessOnboardingToken({ token }); await confirmKeylessOnboardingPin({ pin, action: EKeylessFinalizeAction.Restore, }); }, - [confirmKeylessOnboardingPin, handleKeylessOnboardingTimeout], + [ + confirmKeylessOnboardingPin, + handleKeylessOnboardingTimeout, + intl, + navigation, + ], ); return { @@ -728,8 +765,7 @@ export function useKeylessWallet() { enableKeylessWalletLoading, goToOneKeyIDLoginPageForKeylessWallet, checkKeylessWalletLocalExistence, // step1 - checkKeylessWalletInitedOnServer, // step2 - checkKeylessWalletInitedOnServerForResetPin, // step2 for Reset PIN flow + checkKeylessWalletInitedOnServer, // step2 (handles all modes: default, ResetPin, VerifyPinOnly) confirmKeylessOnboardingPin, // step3 verifyKeylessOnboardingPin, finalizeKeylessWalletV2, // step4 diff --git a/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/WalletEditButton.tsx b/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/WalletEditButton.tsx index c29d031d105e..ae5e5d724b41 100644 --- a/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/WalletEditButton.tsx +++ b/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/WalletEditButton.tsx @@ -4,20 +4,16 @@ import { useIntl } from 'react-intl'; import { ActionList, Divider } from '@onekeyhq/components'; import { AccountSelectorProviderMirror } from '@onekeyhq/kit/src/components/AccountSelector'; +import { useKeylessWallet } from '@onekeyhq/kit/src/components/KeylessWallet/useKeylessWallet'; import { ListItem } from '@onekeyhq/kit/src/components/ListItem'; import { useOneKeyAuth } from '@onekeyhq/kit/src/components/OneKeyAuth/useOneKeyAuth'; -import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; import { useAccountSelectorContextData, useActiveAccount, } from '@onekeyhq/kit/src/states/jotai/contexts/accountSelector'; import type { IDBWallet } from '@onekeyhq/kit-bg/src/dbs/local/types'; import { ETranslations } from '@onekeyhq/shared/src/locale'; -import { - EModalRoutes, - EOnboardingV2KeylessWalletCreationMode, -} from '@onekeyhq/shared/src/routes'; -import { EPrimePages } from '@onekeyhq/shared/src/routes/prime'; +import { EOnboardingV2OneKeyIDLoginMode } from '@onekeyhq/shared/src/routes'; import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; import { usePrimeAvailable } from '../../../Prime/hooks/usePrimeAvailable'; @@ -41,11 +37,11 @@ function WalletEditButtonView({ const { activeAccount: { network }, } = useActiveAccount({ num: num ?? 0 }); - const navigation = useAppNavigation(); const isKeyless = useMemo(() => wallet?.isKeyless, [wallet]); const { isPrimeAvailable } = usePrimeAvailable(); const { user } = useOneKeyAuth(); + const { goToOneKeyIDLoginPageForKeylessWallet } = useKeylessWallet(); const isPrimeUser = useMemo(() => { return user?.primeSubscription?.isActive && user?.onekeyUserId; @@ -123,18 +119,29 @@ function WalletEditButtonView({ onClose={handleActionListClose} /> - {/* Keyless wallet: Keys & Recovery */} + {/* Keyless wallet: Reset PIN */} + {isKeyless ? ( + { + goToOneKeyIDLoginPageForKeylessWallet({ + mode: EOnboardingV2OneKeyIDLoginMode.KeylessResetPin, + }); + }} + /> + ) : null} + + {/* Keyless wallet: Verify PIN */} {isKeyless ? ( { - navigation.push(EModalRoutes.PrimeModal, { - screen: EPrimePages.KeylessWallet, - params: { - mode: EOnboardingV2KeylessWalletCreationMode.View, - }, + goToOneKeyIDLoginPageForKeylessWallet({ + mode: EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly, }); }} /> @@ -208,7 +215,8 @@ function WalletEditButtonView({ showAddHiddenWalletButton, showRemoveWalletButton, showRemoveDeviceButton, - navigation, + goToOneKeyIDLoginPageForKeylessWallet, + intl, ], ); diff --git a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx index 5b163adb6826..773c03c2752c 100644 --- a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx @@ -122,25 +122,13 @@ function OneKeyIDLoginPage() { const intl = useIntl(); const { logout, signInWithSocialLogin } = useOneKeyAuth(); - const { - checkKeylessWalletInitedOnServer, - checkKeylessWalletInitedOnServerForResetPin, - } = useKeylessWallet(); + const { checkKeylessWalletInitedOnServer } = useKeylessWallet(); const goToInputPinPage = useCallback( async ({ token }: { token: string }) => { - // For Reset PIN mode, navigate directly to CreatePin with ResetPin action - if (mode === EOnboardingV2OneKeyIDLoginMode.ResetPin) { - await checkKeylessWalletInitedOnServerForResetPin({ token }); - } else { - await checkKeylessWalletInitedOnServer({ token }); - } + await checkKeylessWalletInitedOnServer({ token, mode }); }, - [ - checkKeylessWalletInitedOnServer, - checkKeylessWalletInitedOnServerForResetPin, - mode, - ], + [checkKeylessWalletInitedOnServer, mode], ); const handleSocialLogin = useCallback( diff --git a/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx b/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx index eff82b39aed0..0a2521d15087 100644 --- a/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/VerifyPinPage.tsx @@ -6,7 +6,10 @@ import { useIntl } from 'react-intl'; import { ETranslations } from '@onekeyhq/shared/src/locale'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; import type { IOnboardingParamListV2 } from '@onekeyhq/shared/src/routes'; -import { EOnboardingPagesV2 } from '@onekeyhq/shared/src/routes'; +import { + EOnboardingPagesV2, + EOnboardingV2OneKeyIDLoginMode, +} from '@onekeyhq/shared/src/routes'; import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; import { AccountSelectorProviderMirror } from '../../../components/AccountSelector/AccountSelectorProvider'; @@ -37,12 +40,15 @@ function VerifyPinPage() { const navigation = useAppNavigation(); const route = useRoute>(); - const { verifyType = 'socialLogin' } = route.params ?? {}; + const { mode } = route.params ?? {}; const { verifyKeylessOnboardingPin } = useKeylessWallet(); const [isLoading, setIsLoading] = useState(false); const pinInputRef = useRef(null); - const isSocialLogin = verifyType === 'socialLogin'; + const isVerifyPinOnly = + mode === EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly; + // Social login mode: when mode is not VerifyPinOnly (or no mode specified) + const isSocialLogin = !isVerifyPinOnly; const [pin, setPin] = useState(''); const [errorMessage, setErrorMessage] = useState(''); @@ -134,7 +140,7 @@ function VerifyPinPage() { const handleVerify = useCallback(async () => { try { setIsLoading(true); - await verifyKeylessOnboardingPin({ pin }); + await verifyKeylessOnboardingPin({ pin, mode }); } finally { setIsLoading(false); setPin(''); @@ -149,7 +155,7 @@ function VerifyPinPage() { platformEnv.isNative ? 100 : 50, ); } - }, [pin, pinInputRef, verifyKeylessOnboardingPin]); + }, [pin, mode, pinInputRef, verifyKeylessOnboardingPin]); const _handleVerifyLegacy = useCallback(() => { // TODO: Verify against actual stored PIN on server diff --git a/packages/kit/src/views/Setting/pages/Tab/config.tsx b/packages/kit/src/views/Setting/pages/Tab/config.tsx index c78eff188e83..cbabc97f3d5b 100644 --- a/packages/kit/src/views/Setting/pages/Tab/config.tsx +++ b/packages/kit/src/views/Setting/pages/Tab/config.tsx @@ -526,7 +526,7 @@ export const useSettingsConfig: () => ISettingsConfig = () => { title: intl.formatMessage({ id: ETranslations.reset_pin }), onPress: (navigation) => { goToOneKeyIDLoginPageForKeylessWallet({ - mode: EOnboardingV2OneKeyIDLoginMode.ResetPin, + mode: EOnboardingV2OneKeyIDLoginMode.KeylessResetPin, }); }, }, diff --git a/packages/shared/src/routes/onboardingv2.ts b/packages/shared/src/routes/onboardingv2.ts index 4b96316d3e32..a25463754fda 100644 --- a/packages/shared/src/routes/onboardingv2.ts +++ b/packages/shared/src/routes/onboardingv2.ts @@ -21,9 +21,9 @@ export enum EOnboardingV2KeylessWalletCreationMode { } export enum EOnboardingV2OneKeyIDLoginMode { - CreateOrImportKeylessWallet = 'CreateOrImportKeylessWallet', - VerifyKeylessWallet = 'VerifyKeylessWallet', - ResetPin = 'ResetPin', + KeylessCreateOrRestore = 'KeylessCreateOrRestore', + KeylessResetPin = 'KeylessResetPin', + KeylessVerifyPinOnly = 'KeylessVerifyPinOnly', } export enum EOnboardingPagesV2 { @@ -140,11 +140,7 @@ export type IOnboardingParamListV2 = { action: EKeylessFinalizeAction; }; [EOnboardingPagesV2.VerifyPin]: { - /** - * 'socialLogin' - User verifies PIN after social login (with retry mechanism) - * 'periodic' - App periodically asks user to verify PIN (no retry mechanism) - */ - verifyType?: 'socialLogin' | 'periodic'; + mode?: EOnboardingV2OneKeyIDLoginMode; }; [EOnboardingPagesV2.ResetPin]: undefined; [EOnboardingPagesV2.NewPinCreated]: undefined; From 9bd8b5f119eb1c3d5f1e3449dc6def4db6c1804e Mon Sep 17 00:00:00 2001 From: morizon Date: Mon, 29 Dec 2025 20:48:08 +0800 Subject: [PATCH 39/66] feat: improve Keyless Wallet onboarding by enhancing PIN verification dialog and refactoring login state management; update error handling in ServiceKeylessWallet for better robustness --- .../ServiceKeylessWallet.ts | 5 ++++- .../KeylessWallet/useKeylessWallet.tsx | 5 ++++- .../Onboardingv2/pages/OneKeyIDLoginPage.tsx | 18 ++++++++++-------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts index 9e73373e608f..38005cca9d3c 100644 --- a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts +++ b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts @@ -1207,7 +1207,10 @@ class ServiceKeylessWallet extends ServiceBase { // For now, verify PIN from mock cache const mockedShares = await this.getMockedKeylessShares(); const juiceboxShare = mockedShares[ownerId]?.juiceboxShare; - if (!juiceboxShare || juiceboxShare?.pin !== pin) { + if (!juiceboxShare) { + throw new OneKeyLocalError('Juicebox share not found'); + } + if (juiceboxShare?.pin !== pin) { throw new OneKeyLocalError('Invalid PIN'); } } diff --git a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx index 7a13e5237618..ac5787ac6f26 100644 --- a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx +++ b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx @@ -730,14 +730,17 @@ export function useKeylessWallet() { // VerifyPinOnly: just verify, show success dialog and close modal if (mode === EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly) { + navigation.popStack(); + Dialog.show({ title: 'PIN Verified', + description: 'PIN verified successfully', showCancelButton: false, onConfirmText: intl.formatMessage({ id: ETranslations.global_got_it, }), onConfirm: () => { - navigation.popStack(); + // }, }); return; diff --git a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx index 773c03c2752c..9e246c64043f 100644 --- a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx @@ -17,9 +17,9 @@ import { EOAuthSocialLoginProvider } from '@onekeyhq/shared/src/consts/authConst import { ETranslations } from '@onekeyhq/shared/src/locale/enum/translations'; import type { EOnboardingPagesV2, + EOnboardingV2OneKeyIDLoginMode, IOnboardingParamListV2, } from '@onekeyhq/shared/src/routes'; -import { EOnboardingV2OneKeyIDLoginMode } from '@onekeyhq/shared/src/routes'; import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; import { AccountSelectorProviderMirror } from '../../../components/AccountSelector/AccountSelectorProvider'; @@ -111,13 +111,14 @@ function OptionItem({ function OneKeyIDLoginPage() { const navigation = useAppNavigation(); - const [isLoggingIn, setIsLoggingIn] = useState(false); + const [loggingInProvider, setLoggingInProvider] = + useState(null); const route = useAppRoute< IOnboardingParamListV2, EOnboardingPagesV2.OneKeyIDLogin >(); - const isLoggingInRef = useRef(false); - isLoggingInRef.current = isLoggingIn; + const loggingInProviderRef = useRef(null); + loggingInProviderRef.current = loggingInProvider; const mode: EOnboardingV2OneKeyIDLoginMode | undefined = route?.params?.mode; const intl = useIntl(); @@ -133,11 +134,11 @@ function OneKeyIDLoginPage() { const handleSocialLogin = useCallback( async (provider: EOAuthSocialLoginProvider) => { - if (isLoggingInRef.current) { + if (loggingInProviderRef.current) { return; } try { - setIsLoggingIn(true); + setLoggingInProvider(provider); const result = await signInWithSocialLogin(provider); if (result?.session?.accessToken) { await goToInputPinPage({ @@ -145,7 +146,7 @@ function OneKeyIDLoginPage() { }); } } finally { - setIsLoggingIn(false); + setLoggingInProvider(null); } }, [goToInputPinPage, signInWithSocialLogin], @@ -180,7 +181,7 @@ function OneKeyIDLoginPage() { icon="GoogleIllus" title="Google" onPress={handleGoogleLogin} - isLoading={isLoggingIn} + isLoading={loggingInProvider === EOAuthSocialLoginProvider.Google} /> From fbebb407d70790decd80b02754de2aad29896c12 Mon Sep 17 00:00:00 2001 From: morizon Date: Mon, 29 Dec 2025 21:20:53 +0800 Subject: [PATCH 40/66] refactor: rename functions and update logic in Keyless Wallet service for clarity; enhance onboarding flow by checking wallet creation status on the server --- .../ServiceKeylessWallet.ts | 171 ++++++++++-------- .../KeylessWallet/useKeylessWallet.tsx | 13 +- .../Onboardingv2/pages/OneKeyIDLoginPage.tsx | 14 +- 3 files changed, 107 insertions(+), 91 deletions(-) diff --git a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts index 38005cca9d3c..1af48100e495 100644 --- a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts +++ b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts @@ -1161,9 +1161,7 @@ class ServiceKeylessWallet extends ServiceBase { return `${provider}:${socialAccountId}`; } - @backgroundMethod() - @toastIfError() - async apiGetKeylessBackendShare(params: { + private async apiGetKeylessBackendShare(params: { token: string; }): Promise { await timerUtils.wait(1500, { devOnly: true }); @@ -1178,9 +1176,7 @@ class ServiceKeylessWallet extends ServiceBase { return mockedShares[ownerId]?.backendShare || null; } - @backgroundMethod() - @toastIfError() - async apiGetKeylessJuiceboxShare(params: { + private async apiGetKeylessJuiceboxShare(params: { token: string; pin: string; }): Promise { @@ -1279,6 +1275,74 @@ class ServiceKeylessWallet extends ServiceBase { return juiceboxShareData; } + /** + * Reset PIN for keyless wallet. + * This method: + * 1. Gets ownerId from social login token + * 2. Gets backendShare from server + * 3. Gets mnemonicPassword from secure storage + * 4. Recovers juiceboxShare using mnemonicPassword + backendShare + * 5. Uploads juiceboxShare with new PIN + */ + @backgroundMethod() + @toastIfError() + async resetKeylessWalletPin(params: { + token: string | undefined; + newPin: string | undefined; + }) { + const { token, newPin } = params; + if (!token) { + throw new OneKeyLocalError('social login token is required'); + } + if (!newPin) { + throw new OneKeyLocalError('new PIN is required'); + } + + // 1. Get ownerId from token + const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); + + // 2. Get backendShare from server + const backendShareData = await this.apiGetKeylessBackendShare({ token }); + if (!backendShareData) { + throw new OneKeyLocalError('Backend share not found'); + } + + // 3. Get mnemonicPassword from secure storage + const mnemonicPassword = + await keylessMnemonicPasswordStorage.getMnemonicPasswordFromStorage({ + ownerId, + backgroundApi: this.backgroundApi, + }); + if (!mnemonicPassword) { + throw new OneKeyLocalError( + 'Mnemonic password not found in storage. Please restore the wallet first.', + ); + } + + // 4. Get x-coordinates from stored data + // juiceboxShareX is stored in backendShareData for recovery + const backendShareX = keylessWalletUtils.getShareXCoordinate( + backendShareData.backendShare, + ); + + // 5. Recover juiceboxShare using recoverMissingShareFromSecret + const juiceboxShare = await this.recoverMissingShareFromSecret({ + secretBase64: mnemonicPassword, + shareBase64: backendShareData.backendShare, + missingX: backendShareData.juiceboxShareX, + }); + + // 6. Upload juiceboxShare with new PIN + await this.apiUploadKeylessJuiceboxShare({ + token, + juiceboxShare, + pin: newPin, + backendShareX, + }); + + return { success: true }; + } + @backgroundMethod() @toastIfError() async restoreKeylessWalletFromServer(params: { @@ -1298,7 +1362,7 @@ class ServiceKeylessWallet extends ServiceBase { const mockedShares = await this.getMockedKeylessShares(); const existingShares = mockedShares[ownerId]; if (!existingShares?.backendShare || !existingShares?.juiceboxShare) { - throw new OneKeyLocalError('Keyless wallet not initialized'); + throw new OneKeyLocalError('Keyless wallet not created'); } // Get backend share from server @@ -1350,78 +1414,9 @@ class ServiceKeylessWallet extends ServiceBase { }; } - /** - * Reset PIN for keyless wallet. - * This method: - * 1. Gets ownerId from social login token - * 2. Gets backendShare from server - * 3. Gets mnemonicPassword from secure storage - * 4. Recovers juiceboxShare using mnemonicPassword + backendShare - * 5. Uploads juiceboxShare with new PIN - */ - @backgroundMethod() - @toastIfError() - async resetKeylessWalletPin(params: { - token: string | undefined; - newPin: string | undefined; - }) { - const { token, newPin } = params; - if (!token) { - throw new OneKeyLocalError('social login token is required'); - } - if (!newPin) { - throw new OneKeyLocalError('new PIN is required'); - } - - // 1. Get ownerId from token - const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); - - // 2. Get backendShare from server - const backendShareData = await this.apiGetKeylessBackendShare({ token }); - if (!backendShareData) { - throw new OneKeyLocalError('Backend share not found'); - } - - // 3. Get mnemonicPassword from secure storage - const mnemonicPassword = - await keylessMnemonicPasswordStorage.getMnemonicPasswordFromStorage({ - ownerId, - backgroundApi: this.backgroundApi, - }); - if (!mnemonicPassword) { - throw new OneKeyLocalError( - 'Mnemonic password not found in storage. Please restore the wallet first.', - ); - } - - // 4. Get x-coordinates from stored data - // juiceboxShareX is stored in backendShareData for recovery - const { juiceboxShareX } = backendShareData; - const backendShareX = keylessWalletUtils.getShareXCoordinate( - backendShareData.backendShare, - ); - - // 5. Recover juiceboxShare using recoverMissingShareFromSecret - const juiceboxShare = await this.recoverMissingShareFromSecret({ - secretBase64: mnemonicPassword, - shareBase64: backendShareData.backendShare, - missingX: juiceboxShareX, - }); - - // 6. Upload juiceboxShare with new PIN - await this.apiUploadKeylessJuiceboxShare({ - token, - juiceboxShare, - pin: newPin, - backendShareX, - }); - - return { success: true }; - } - @backgroundMethod() @toastIfError() - async initKeylessWalletToServer(params: { + async createKeylessWalletToServer(params: { token: string | undefined; pin: string | undefined; customMnemonic?: string; @@ -1437,12 +1432,14 @@ class ServiceKeylessWallet extends ServiceBase { const mockedShares = await this.getMockedKeylessShares(); const existingShares = mockedShares[ownerId]; if (existingShares?.backendShare || existingShares?.juiceboxShare) { - throw new OneKeyLocalError('Keyless wallet already initialized'); + throw new OneKeyLocalError('Keyless wallet already created'); } - let mnemonic: string = generateMnemonic(256); + let mnemonic = ''; const devSettings = await devSettingsPersistAtom.get(); if (devSettings.enabled && customMnemonic && customMnemonic.trim()) { mnemonic = customMnemonic.trim(); + } else { + mnemonic = generateMnemonic(256); } const mnemonicPasswordBytes = crypto.getRandomValues(new Uint8Array(32)); const mnemonicPassword = bufferUtils.bytesToBase64(mnemonicPasswordBytes); @@ -1478,6 +1475,7 @@ class ServiceKeylessWallet extends ServiceBase { mnemonicPassword, backgroundApi: this.backgroundApi, }); + // TODO verify mnemonicPassword is saved successfully const _backendShareData: IKeylessBackendShare = await this.apiUploadKeylessBackendShare({ @@ -1487,6 +1485,7 @@ class ServiceKeylessWallet extends ServiceBase { juiceboxShareX, // Store the other share's x-coordinate for recovery }); // TODO verify backendShareData is valid + const _juiceboxShareData: IKeylessJuiceboxShare = await this.apiUploadKeylessJuiceboxShare({ token, @@ -1501,6 +1500,20 @@ class ServiceKeylessWallet extends ServiceBase { text: mnemonic, }), }; + // TODO cleanup if error occurs + } + + @backgroundMethod() + @toastIfError() + async isKeylessWalletCreatedOnServer(params: { + token: string; + }): Promise { + const { token } = params; + const backendShareInfo = await this.apiGetKeylessBackendShare({ + token, + }); + const isCreated = !!backendShareInfo; + return isCreated; } } diff --git a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx index ac5787ac6f26..a8d6ce1d176f 100644 --- a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx +++ b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx @@ -577,7 +577,7 @@ export function useKeylessWallet() { }); }, [goToOneKeyIDLoginPageForKeylessWallet, intl]); - const checkKeylessWalletInitedOnServer = useCallback( + const checkKeylessWalletCreatedOnServer = useCallback( async ({ token, mode, @@ -608,14 +608,13 @@ export function useKeylessWallet() { } // Default: check wallet existence and navigate accordingly - const backendShareInfo = - await backgroundApiProxy.serviceKeylessWallet.apiGetKeylessBackendShare( + const isCreated = + await backgroundApiProxy.serviceKeylessWallet.isKeylessWalletCreatedOnServer( { token, }, ); - const isInited = !!backendShareInfo; - if (isInited) { + if (isCreated) { navigation.push(EOnboardingPagesV2.VerifyPin); } else { navigation.push(EOnboardingPagesV2.CreatePin); @@ -662,7 +661,7 @@ export function useKeylessWallet() { if (action === EKeylessFinalizeAction.Create) { const customMnemonic = await getKeylessOnboardingCustomMnemonic(); ({ mnemonic } = - await backgroundApiProxy.serviceKeylessWallet.initKeylessWalletToServer( + await backgroundApiProxy.serviceKeylessWallet.createKeylessWalletToServer( { token, pin, @@ -768,7 +767,7 @@ export function useKeylessWallet() { enableKeylessWalletLoading, goToOneKeyIDLoginPageForKeylessWallet, checkKeylessWalletLocalExistence, // step1 - checkKeylessWalletInitedOnServer, // step2 (handles all modes: default, ResetPin, VerifyPinOnly) + checkKeylessWalletCreatedOnServer, // step2 (handles all modes: default, ResetPin, VerifyPinOnly) confirmKeylessOnboardingPin, // step3 verifyKeylessOnboardingPin, finalizeKeylessWalletV2, // step4 diff --git a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx index 9e246c64043f..3fd4b36bf0a4 100644 --- a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx @@ -123,13 +123,13 @@ function OneKeyIDLoginPage() { const intl = useIntl(); const { logout, signInWithSocialLogin } = useOneKeyAuth(); - const { checkKeylessWalletInitedOnServer } = useKeylessWallet(); + const { checkKeylessWalletCreatedOnServer } = useKeylessWallet(); const goToInputPinPage = useCallback( async ({ token }: { token: string }) => { - await checkKeylessWalletInitedOnServer({ token, mode }); + await checkKeylessWalletCreatedOnServer({ token, mode }); }, - [checkKeylessWalletInitedOnServer, mode], + [checkKeylessWalletCreatedOnServer, mode], ); const handleSocialLogin = useCallback( @@ -181,7 +181,9 @@ function OneKeyIDLoginPage() { icon="GoogleIllus" title="Google" onPress={handleGoogleLogin} - isLoading={loggingInProvider === EOAuthSocialLoginProvider.Google} + isLoading={ + loggingInProvider === EOAuthSocialLoginProvider.Google + } /> From 052faa166a6e1a3e4e98c9318e4368216dc98efa Mon Sep 17 00:00:00 2001 From: Franco Date: Tue, 30 Dec 2025 16:56:49 +0800 Subject: [PATCH 41/66] animations & account selector --- .../components/WalletAvatar/WalletAvatar.tsx | 11 ++- .../WalletEdit/WalletRemoveButton.tsx | 15 +++- .../WalletEdit/WalletRemoveDialog.tsx | 8 +-- .../pages/CreateOrImportWallet.tsx | 35 ++++++--- .../views/Onboardingv2/pages/GetStarted.tsx | 29 ++++++-- .../shared/src/locale/enum/translations.ts | 72 +++++++++++++++++++ packages/shared/src/locale/json/bn.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/de.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/en_US.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/es.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/fr_FR.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/hi_IN.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/id.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/it_IT.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/ja_JP.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/ko_KR.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/pt.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/pt_BR.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/ru.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/th_TH.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/uk_UA.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/vi.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/zh_CN.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/zh_HK.json | 72 +++++++++++++++++++ packages/shared/src/locale/json/zh_TW.json | 72 +++++++++++++++++++ 25 files changed, 1518 insertions(+), 20 deletions(-) diff --git a/packages/kit/src/components/WalletAvatar/WalletAvatar.tsx b/packages/kit/src/components/WalletAvatar/WalletAvatar.tsx index ea58ed29cdd9..e49fac09107a 100644 --- a/packages/kit/src/components/WalletAvatar/WalletAvatar.tsx +++ b/packages/kit/src/components/WalletAvatar/WalletAvatar.tsx @@ -5,6 +5,7 @@ import type { SizeTokens } from '@onekeyhq/components'; import { Icon, Image, SizableText, Stack } from '@onekeyhq/components'; import type { IDBWallet } from '@onekeyhq/kit-bg/src/dbs/local/types'; import { presetNetworksMap } from '@onekeyhq/shared/src/config/presetNetworks'; +import { EOAuthSocialLoginProvider } from '@onekeyhq/shared/src/consts/authConsts'; import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; import type { IAllWalletAvatarImageNames } from '@onekeyhq/shared/src/utils/avatarUtils'; import { AllWalletAvatarImages } from '@onekeyhq/shared/src/utils/avatarUtils'; @@ -23,6 +24,7 @@ export type IWalletAvatarProps = IWalletAvatarBaseProps & { status?: IWalletProps['status']; badge?: number | string; firmwareTypeBadge?: EFirmwareType; + socialLoginProvider?: EOAuthSocialLoginProvider; }; export function WalletAvatarBase({ @@ -65,6 +67,7 @@ export function WalletAvatar({ status, badge, firmwareTypeBadge, + socialLoginProvider, img, wallet, }: IWalletAvatarProps) { @@ -115,7 +118,7 @@ export function WalletAvatar({ ) : null} - {/* Keyless wallet cloud icon */} + {/* Keyless wallet social login provider icon */} {status === 'keyless' ? ( - + {socialLoginProvider === EOAuthSocialLoginProvider.Google ? ( + + ) : ( + + )} ) : null} diff --git a/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/WalletRemoveButton.tsx b/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/WalletRemoveButton.tsx index ca1110da47ba..46ada85cd736 100644 --- a/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/WalletRemoveButton.tsx +++ b/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/WalletRemoveButton.tsx @@ -30,6 +30,9 @@ export function WalletRemoveButton({ if (platformEnv.isWebDappMode) { return intl.formatMessage({ id: ETranslations.explore_disconnect }); } + if (wallet?.isKeyless) { + return intl.formatMessage({ id: ETranslations.log_out_wallet }); + } if (accountUtils.isHwHiddenWallet({ wallet })) { return intl.formatMessage({ id: ETranslations.remove_hidden_wallet }); } @@ -46,9 +49,19 @@ export function WalletRemoveButton({ }); }, [isRemoveToMocked, wallet, intl]); + const icon = useMemo(() => { + if (wallet?.isKeyless) { + return 'LogoutOutline'; + } + if (isRemoveToMocked) { + return 'DeleteOutline'; + } + return 'EjectOutline'; + }, [wallet?.isKeyless, isRemoveToMocked]); + return ( ) : null} */} - {enableKeylessWalletLoading ? ( - - ) : ( - - )} + + {enableKeylessWalletLoading ? ( + + + + ) : ( + + + + )} + diff --git a/packages/kit/src/views/Onboardingv2/pages/GetStarted.tsx b/packages/kit/src/views/Onboardingv2/pages/GetStarted.tsx index ae1243d45665..8033cfce0ee5 100644 --- a/packages/kit/src/views/Onboardingv2/pages/GetStarted.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/GetStarted.tsx @@ -14,6 +14,7 @@ import Svg, { import type { IYStackProps } from '@onekeyhq/components'; import { + AnimatePresence, BlurView, Button, DecorativeOneKeyLogo, @@ -447,11 +448,29 @@ export default function GetStarted() { onPress={isGoogleLoading ? undefined : handleGoogleLogin} > - {isGoogleLoading ? ( - - ) : ( - - )} + + {isGoogleLoading ? ( + + + + ) : ( + + + + )} + {intl.formatMessage( { id: ETranslations.continue_with_social_platform }, diff --git a/packages/shared/src/locale/enum/translations.ts b/packages/shared/src/locale/enum/translations.ts index 41ddf186af62..53d797f3de46 100644 --- a/packages/shared/src/locale/enum/translations.ts +++ b/packages/shared/src/locale/enum/translations.ts @@ -42,6 +42,12 @@ Limit_order_status_filled = 'Limit.order_status_filled', Limit_order_status_open = 'Limit.order_status_open', Limit_order_status_unfilled = 'Limit.order_status_unfilled', + Perps_BBO_Counterparty = 'Perps.BBO_Counterparty', + Perps_BBO_Queue = 'Perps.BBO_Queue', + Perps_BBO_button_desc = 'Perps.BBO_button_desc', + Perps_BBO_button_title = 'Perps.BBO_button_title', + Perps_BBO_select_title = 'Perps.BBO_select_title', + Perps_BBO_unavailable = 'Perps.BBO_unavailable', Perps_referral_bonus_from = 'Perps.referral_bonus_from', account_model_watched = 'account_model.watched', account_name_form_helper_text = 'account_name_form_helper_text', @@ -457,9 +463,66 @@ date_today = 'date.today', date_yesterday = 'date.yesterday', defi_apr_apy = 'defi.apr_apy', + defi_asset_borrowed = 'defi.asset_borrowed', + defi_asset_can_be_collateral = 'defi.asset_can_be_collateral', + defi_asset_supplied = 'defi.asset_supplied', + defi_assets_to_borrow = 'defi.assets_to_borrow', + defi_assets_to_supply = 'defi.assets_to_supply', + defi_available_liquidity = 'defi.available_liquidity', + defi_available_to_borrow = 'defi.available_to_borrow', + defi_borrow_apy = 'defi.borrow_apy', + defi_borrow_cap_usage = 'defi.borrow_cap_usage', + defi_borrowable = 'defi.borrowable', + defi_borrowable_today = 'defi.borrowable_today', + defi_borrowed = 'defi.borrowed', + defi_borrowed_balance = 'defi.borrowed_balance', + defi_can_be_collateral = 'defi.can_be_collateral', + defi_claim_all = 'defi.claim_all', + defi_claimable_rewards = 'defi.claimable_rewards', + defi_current_utilization = 'defi.current_utilization', + defi_daily_borrow_cap = 'defi.daily_borrow_cap', + defi_daily_cap_resets_in = 'defi.daily_cap_resets_in', + defi_daily_caps = 'defi.daily_caps', + defi_daily_withdraw_cap = 'defi.daily_withdraw_cap', + defi_from_wallet_balance = 'defi.from_wallet_balance', + defi_health_factor = 'defi.health_factor', + defi_interest_rate_model = 'defi.interest_rate_model', defi_liquidation_acknowledge = 'defi.liquidation_acknowledge', + defi_liquidation_at_less_than_1_00 = 'defi.liquidation_at_less_than_1_00', defi_liquidation_borrow_desc = 'defi.liquidation_borrow_desc', + defi_liquidation_ltv = 'defi.liquidation_ltv', defi_liquidation_withdraw_desc = 'defi.liquidation_withdraw_desc', + defi_manage_position = 'defi.manage_position', + defi_max_ltv = 'defi.max_ltv', + defi_my_borrow = 'defi.my_borrow', + defi_my_info = 'defi.my_info', + defi_my_supply = 'defi.my_supply', + defi_net_apy = 'defi.net_apy', + defi_net_worth = 'defi.net_worth', + defi_no_assets_to_borrow = 'defi.no_assets_to_borrow', + defi_no_assets_to_supply = 'defi.no_assets_to_supply', + defi_nothing_supplied_yet = 'defi.nothing_supplied_yet', + defi_oracle_price = 'defi.oracle_price', + defi_platform_bonus = 'defi.platform_bonus', + defi_refundable_fee = 'defi.refundable_fee', + defi_repay = 'defi.repay', + defi_reserve_size = 'defi.reserve_size', + defi_safe_max = 'defi.safe_max', + defi_select_an_asset_to_borrow = 'defi.select_an_asset_to_borrow', + defi_select_an_asset_to_supply = 'defi.select_an_asset_to_supply', + defi_show_assets_with_0_balance = 'defi.show_assets_with_0_balance', + defi_soft_liquidations = 'defi.soft_liquidations', + defi_supplied = 'defi.supplied', + defi_supplied_balance = 'defi.supplied_balance', + defi_supply = 'defi.supply', + defi_supply_apy = 'defi.supply_apy', + defi_supply_assets_as_collateral_before_borrowing = 'defi.supply_assets_as_collateral_before_borrowing', + defi_supply_cap_usage = 'defi.supply_cap_usage', + defi_use_as_collateral = 'defi.use_as_collateral', + defi_utilization_ratio = 'defi.utilization_ratio', + defi_view_reserve_details = 'defi.view_reserve_details', + defi_with_collateral = 'defi.with_collateral', + defi_withdrawable_today = 'defi.withdrawable_today', derivation_path = 'derivation_path', description_403 = 'description_403', device_btc_only_coming_soon = 'device.btc_only_coming_soon', @@ -1167,6 +1230,7 @@ global_approvals = 'global.approvals', global_approve = 'global.approve', global_apr = 'global.apr', + global_apy = 'global.apy', global_asset = 'global.asset', global_at_least_variable_characters = 'global.at_least_variable_characters', global_auto = 'global.auto', @@ -1187,6 +1251,7 @@ global_bluetooth = 'global.bluetooth', global_bluetooth_firmware = 'global.bluetooth_firmware', global_bootloader = 'global.bootloader', + global_borrow = 'global.borrow', global_brightness = 'global.brightness', global_browser = 'global.browser', global_bulk = 'global.bulk', @@ -1816,6 +1881,7 @@ global_wallet = 'global.wallet', global_wallet_activity = 'global.wallet_activity', global_wallet_avatar = 'global.wallet_avatar', + global_wallet_balance = 'global.wallet_balance', global_wallet_history_notification_banner = 'global.wallet_history_notification_banner', global_wallets = 'global.wallets', global_wallpaper = 'global.wallpaper', @@ -2020,6 +2086,8 @@ ln_authorize_access_network_error = 'ln.authorize_access_network_error', ln_payment_received_label = 'ln.payment_received_label', log_out_confirmation_text = 'log_out_confirmation_text', + log_out_wallet = 'log_out_wallet', + log_out_wallet_desc = 'log_out_wallet_desc', logged_out_feedback = 'logged_out_feedback', login_forgot_passcode = 'login.forgot_passcode', login_forgot_password = 'login.forgot_password', @@ -2056,6 +2124,7 @@ market_24h_txns = 'market.24h_txns', market_24h_vol_usd = 'market.24h_vol_usd', market_30d = 'market.30d', + market_3m = 'market.3m', market_7d = 'market.7d', market_add_number_tokens = 'market.add_number_tokens', market_add_to_favorites = 'market.add_to_favorites', @@ -3158,8 +3227,11 @@ send_password_validation = 'send.password_validation', send_preview_button = 'send.preview_button', send_recipient_invalid = 'send.recipient_invalid', + send_recipient_token_not_activated = 'send.recipient_token_not_activated', send_send_to_this_address = 'send.send_to_this_address', send_sending_str_requires_an_account_balance_of_at_least_str_str = 'send.sending_str_requires_an_account_balance_of_at_least_str_str', + send_stellar_activation_minimum_hint = 'send.stellar_activation_minimum_hint', + send_stellar_recipient_account_not_activated = 'send.stellar_recipient_account_not_activated', send_str_minimum_balance_is_str = 'send.str_minimum_balance_is_str', send_str_minimum_transfer = 'send.str_minimum_transfer', send_suggest_reserving_str_as_gas_fee = 'send.suggest_reserving_str_as_gas_fee', diff --git a/packages/shared/src/locale/json/bn.json b/packages/shared/src/locale/json/bn.json index 7e9c5a8f4fb7..76fb571358ba 100644 --- a/packages/shared/src/locale/json/bn.json +++ b/packages/shared/src/locale/json/bn.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "পূর্ণ", "Limit.order_status_open": "খুলুন", "Limit.order_status_unfilled": "অপূর্ণ", + "Perps.BBO_Counterparty": "কাউন্টারপার্টি ১", + "Perps.BBO_Queue": "কিউ ১", + "Perps.BBO_button_desc": "বেস্ট-বিড-অফার (BBO) সর্বোত্তম বিড বা আস্ক দামে একটি লিমিট অর্ডার স্থাপন করে। আপনি যদি পোস্ট-অনলি ব্যবহার করেন, তবে আপনার অর্ডার শুধুমাত্র তখনই স্থাপিত হবে যখন এটি তারল্য যোগ করবে। বাজারের ওঠানামার কারণে, অর্ডার নিশ্চিত হওয়ার আগে বাজার নড়াচড়া করলে অর্ডারটি স্থাপনে ব্যর্থ হতে পারে।", + "Perps.BBO_button_title": "সর্বোত্তম মূল্য", + "Perps.BBO_select_title": "BBO মোড নির্বাচন করুন", + "Perps.BBO_unavailable": "সর্বোত্তম মূল্য (BBO) উপলভ্য নয়", "Perps.referral_bonus_from": "পার্পস-এ ট্রেডিং থেকে রেফারাল বোনাস", "account_model.watched": "দেখা হয়েছে", "account_name_form_helper_text": "সংবেদনশীল তথ্য প্রবেশ করাবেন না।", @@ -452,9 +458,66 @@ "date.today": "আজ", "date.yesterday": "গতকাল", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "সম্পদ / ধারকৃত", + "defi.asset_can_be_collateral": "সম্পদ / বন্ধক হিসাবে ব্যবহার করা যাবে", + "defi.asset_supplied": "সম্পদ / সরবরাহকৃত", + "defi.assets_to_borrow": "ধার করার জন্য সম্পদ", + "defi.assets_to_supply": "সরবরাহ করার জন্য সম্পদ", + "defi.available_liquidity": "উপলব্ধ তারল্য", + "defi.available_to_borrow": "ধার করার জন্য উপলব্ধ", + "defi.borrow_apy": "ধার APY", + "defi.borrow_cap_usage": "ধার সীমা ব্যবহার", + "defi.borrowable": "ধারযোগ্য", + "defi.borrowable_today": "আজ ধারযোগ্য", + "defi.borrowed": "ধারকৃত", + "defi.borrowed_balance": "ধার করা ব্যালেন্স", + "defi.can_be_collateral": "জামানত হতে পারে", + "defi.claim_all": "সব দাবি করুন", + "defi.claimable_rewards": "দাবিযোগ্য পুরস্কার", + "defi.current_utilization": "বর্তমান ব্যবহার", + "defi.daily_borrow_cap": "দৈনিক ধার সীমা", + "defi.daily_cap_resets_in": "দৈনিক সীমা পুনরায় সেট হবে", + "defi.daily_caps": "দৈনিক সীমা", + "defi.daily_withdraw_cap": "দৈনিক উত্তোলন সীমা", + "defi.from_wallet_balance": "ওয়ালেট ব্যালেন্স থেকে", + "defi.health_factor": "স্বাস্থ্য গুণক", + "defi.interest_rate_model": "সুদের হার মডেল", "defi.liquidation_acknowledge": "আমি জড়িত ঝুঁকিগুলি স্বীকার করি", + "defi.liquidation_at_less_than_1_00": "< 1.00-এ লিকুইডেশন", "defi.liquidation_borrow_desc": "এই পরিমাণ ঋণ নিলে আপনার স্বাস্থ্যগত কারণ কমে যাবে এবং ঋণ পরিশোধের ঝুঁকি বাড়বে।", + "defi.liquidation_ltv": "অবসায়ন LTV", "defi.liquidation_withdraw_desc": "এই পরিমাণ টাকা তুলে নিলে আপনার স্বাস্থ্যগত কারণ কমে যাবে এবং লেনদেন বন্ধ হয়ে যাওয়ার ঝুঁকি বাড়বে।", + "defi.manage_position": "অবস্থান পরিচালনা করুন", + "defi.max_ltv": "সর্বোচ্চ LTV", + "defi.my_borrow": "আমার ধার", + "defi.my_info": "আমার তথ্য", + "defi.my_supply": "আমার সরবরাহ", + "defi.net_apy": "নিট এপিওয়াই", + "defi.net_worth": "নিট মূল্য", + "defi.no_assets_to_borrow": "ধার করার জন্য কোনও সম্পদ নেই", + "defi.no_assets_to_supply": "সরবরাহের জন্য কোনও সম্পদ উপলব্ধ নেই", + "defi.nothing_supplied_yet": "এখনো কিছু সরবরাহ করা হয়নি", + "defi.oracle_price": "ওরাকল মূল্য", + "defi.platform_bonus": "প্ল্যাটফর্ম বোনাস", + "defi.refundable_fee": "ফেরতযোগ্য ফি", + "defi.repay": "পরিশোধ করা", + "defi.reserve_size": "রিজার্ভ আকার", + "defi.safe_max": "নিরাপদ সর্বোচ্চ", + "defi.select_an_asset_to_borrow": "ধার করার জন্য একটি সম্পদ নির্বাচন করুন", + "defi.select_an_asset_to_supply": "সরবরাহ করার জন্য একটি সম্পদ নির্বাচন করুন", + "defi.show_assets_with_0_balance": "0 ব্যালেন্স সহ সম্পদ দেখান", + "defi.soft_liquidations": "নরম তরলীকরণ", + "defi.supplied": "সরবরাহকৃত", + "defi.supplied_balance": "সরবরাহকৃত ব্যালেন্স", + "defi.supply": "সরবরাহ", + "defi.supply_apy": "সরবরাহ APY", + "defi.supply_assets_as_collateral_before_borrowing": "ধার করার আগে সম্পদ জামানত হিসাবে সরবরাহ করুন", + "defi.supply_cap_usage": "সরবরাহ সীমা ব্যবহার", + "defi.use_as_collateral": "জামানত হিসাবে ব্যবহার করুন", + "defi.utilization_ratio": "ব্যবহার অনুপাত", + "defi.view_reserve_details": "রিজার্ভ বিবরণ দেখুন", + "defi.with_collateral": "জামানত সহ", + "defi.withdrawable_today": "আজ উত্তোলনযোগ্য", "derivation_path": "উৎপত্তি পথ", "description_403": "আমাদের সেবাগুলি আপনার অঞ্চলে পাওয়া যায় না।", "device.btc_only_coming_soon": "এই ফিচারটি পরবর্তী BTC-Only ফার্মওয়্যার রিলিজে উপলভ্য হবে।", @@ -1162,6 +1225,7 @@ "global.approvals": "অনুমোদনসমূহ", "global.approve": "অনুমোদন করুন", "global.apr": "APR", + "global.apy": "APY", "global.asset": "সম্পত্তি", "global.at_least_variable_characters": "কমপক্ষে {variable} অক্ষর", "global.auto": "অটো", @@ -1182,6 +1246,7 @@ "global.bluetooth": "ব্লুটুথ", "global.bluetooth_firmware": "ব্লুটুথ ফার্মওয়্যার", "global.bootloader": "Bootloader", + "global.borrow": "ধার নিন", "global.brightness": "উজ্জ্বলতা", "global.browser": "ব্রাউজার", "global.bulk": "বাল্ক", @@ -1811,6 +1876,7 @@ "global.wallet": "ওয়ালেট", "global.wallet_activity": "ওয়ালেট কার্যকলাপ", "global.wallet_avatar": "ওয়ালেট অবতার", + "global.wallet_balance": "ওয়ালেট ব্যালেন্স", "global.wallet_history_notification_banner": "আপনার ওয়ালেটের কার্যকলাপ সম্পর্কে তাৎক্ষণিক আপডেট পেতে নোটিফিকেশন চালু করুন।", "global.wallets": "মানিব্যাগ", "global.wallpaper": "ওয়ালপেপার", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "প্রমাণীকরণ ব্যর্থ হয়েছে, আপনার নেটওয়ার্ক সংযোগ পরীক্ষা করুন এবং আবার চেষ্টা করুন", "ln.payment_received_label": "পেমেন্ট পেয়েছি", "log_out_confirmation_text": "আপনি কি নিশ্চিত যে আপনি {email} থেকে লগ আউট করতে চান?", + "log_out_wallet": "ওয়ালেট থেকে লগ আউট করুন", + "log_out_wallet_desc": "এতে এই ডিভাইস থেকে ওয়ালেটটি মুছে যাবে। আপনি যেকোনো সময় আপনার সামাজিক অ্যাকাউন্ট এবং পিন ব্যবহার করে আবার লগইন করতে পারবেন।", "logged_out_feedback": "সফলভাবে লগ আউট হয়েছে", "login.forgot_passcode": "পাসকোড ভুলে গেছেন?", "login.forgot_password": "পাসওয়ার্ড ভুলে গেছেন?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "২৪ ঘণ্টার লেনদেন", "market.24h_vol_usd": "২৪ ঘণ্টার ভলিউম (USD)", "market.30d": "৩০ দিন", + "market.3m": "৩এম", "market.7d": "৭ দিন", "market.add_number_tokens": "{number} টোকেন যোগ করুন", "market.add_to_favorites": "পছন্দের তালিকায় যোগ করুন", @@ -3153,8 +3222,11 @@ "send.password_validation": "পাসওয়ার্ডটি 8 থেকে 128 অক্ষরের মধ্যে হতে হবে।", "send.preview_button": "প্রিভিউ", "send.recipient_invalid": "অবৈধ প্রাপক। দয়া করে পরীক্ষা করুন এবং পুনরায় প্রবেশ করুন", + "send.recipient_token_not_activated": "প্রাপকের অ্যাকাউন্টটি এই টোকেনটি সক্রিয় করেনি। দয়া করে প্রাপককে একটি টোকেন যোগ করতে বলুন।", "send.send_to_this_address": "এই ঠিকানায় পাঠান", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "{0} পাঠানোর জন্য আপনার অ্যাকাউন্টে কমপক্ষে {1} {2} ব্যালেন্স থাকা প্রয়োজন।", + "send.stellar_activation_minimum_hint": "অ্যাকাউন্টটি সক্রিয় করতে কমপক্ষে ১ XLM প্রয়োজন।", + "send.stellar_recipient_account_not_activated": "প্রাপকের অ্যাকাউন্টটি স্টেলার নেটওয়ার্কে সক্রিয় নেই। এটি শুরু করতে দয়া করে এই ঠিকানায় কমপক্ষে ১টি XLM পাঠান।", "send.str_minimum_balance_is_str": "পাঠানো ব্যর্থ হয়েছে। {token} এর জন্য সর্বনিম্ন ব্যালেন্স {amount} হতে হবে।", "send.str_minimum_transfer": "ন্যূনতম স্থানান্তর পরিমাণ {0}।", "send.suggest_reserving_str_as_gas_fee": "{0} নেটওয়ার্ক ফি হিসেবে সংরক্ষণ করার পরামর্শ দিন।", diff --git a/packages/shared/src/locale/json/de.json b/packages/shared/src/locale/json/de.json index 5d97590a78bd..14a8ac1485db 100644 --- a/packages/shared/src/locale/json/de.json +++ b/packages/shared/src/locale/json/de.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "Ausgefüllt", "Limit.order_status_open": "Öffnen", "Limit.order_status_unfilled": "Ungefüllt", + "Perps.BBO_Counterparty": "Gegenpartei 1", + "Perps.BBO_Queue": "Warteschlange 1", + "Perps.BBO_button_desc": "Best-Bid-Offer (BBO) platziert eine Limit-Order zum besten Geld- oder Briefkurs. Wenn Sie \"Nur Posten\" verwenden, wird Ihre Order nur aufgegeben, wenn sie Liquidität hinzufügt. Aufgrund von Marktschwankungen kann die Order fehlschlagen, wenn sich der Markt bewegt, bevor sie bestätigt wird.", + "Perps.BBO_button_title": "BBO", + "Perps.BBO_select_title": "Wähle den BBO-Modus", + "Perps.BBO_unavailable": "Bestes Brief-Gebot nicht verfügbar", "Perps.referral_bonus_from": "Empfehlungsbonus aus dem Handel mit Perps", "account_model.watched": "Gesehen", "account_name_form_helper_text": "Geben Sie keine sensiblen Informationen ein.", @@ -452,9 +458,66 @@ "date.today": "Heute", "date.yesterday": "Gestern", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "Vermögenswert / Ausgeliehen", + "defi.asset_can_be_collateral": "Vermögenswert / Kann als Sicherheit dienen", + "defi.asset_supplied": "Vermögenswert / Geliefert", + "defi.assets_to_borrow": "Vermögenswerte zum Ausleihen", + "defi.assets_to_supply": "Vermögenswerte zum Liefern", + "defi.available_liquidity": "Verfügbare Liquidität", + "defi.available_to_borrow": "Verfügbar zum Ausleihen", + "defi.borrow_apy": "Ausleih-APY", + "defi.borrow_cap_usage": "Nutzung der Ausleih-Obergrenze", + "defi.borrowable": "Ausleihbar", + "defi.borrowable_today": "Heute ausleihbar", + "defi.borrowed": "Ausgeliehen", + "defi.borrowed_balance": "Geliehener Saldo", + "defi.can_be_collateral": "Kann als Sicherheit dienen", + "defi.claim_all": "Alles beanspruchen", + "defi.claimable_rewards": "Einlösbare Belohnungen", + "defi.current_utilization": "Aktuelle Auslastung", + "defi.daily_borrow_cap": "Tägliche Ausleih-Obergrenze", + "defi.daily_cap_resets_in": "Tageslimit wird zurückgesetzt in", + "defi.daily_caps": "Tägliche Obergrenzen", + "defi.daily_withdraw_cap": "Tägliches Auszahlungslimit", + "defi.from_wallet_balance": "Vom Wallet-Guthaben", + "defi.health_factor": "Gesundheitsfaktor", + "defi.interest_rate_model": "Zinsmodell", "defi.liquidation_acknowledge": "Ich bin mir der damit verbundenen Risiken bewusst.", + "defi.liquidation_at_less_than_1_00": "Liquidation bei < 1.00", "defi.liquidation_borrow_desc": "Die Aufnahme eines Kredits in dieser Höhe wird Ihren Gesundheitsfaktor verringern und das Risiko einer Liquidation erhöhen.", + "defi.liquidation_ltv": "Liquidations-LTV", "defi.liquidation_withdraw_desc": "Die Abhebung dieses Betrags wird Ihren Gesundheitsfaktor verringern und das Liquidationsrisiko erhöhen.", + "defi.manage_position": "Position verwalten", + "defi.max_ltv": "Max. Beleihungswert", + "defi.my_borrow": "Meine Ausleihe", + "defi.my_info": "Meine Info", + "defi.my_supply": "Mein Angebot", + "defi.net_apy": "Netto-APY", + "defi.net_worth": "Reinvermögen", + "defi.no_assets_to_borrow": "Keine verfügbaren Vermögenswerte für einen Kredit", + "defi.no_assets_to_supply": "Keine Vermögenswerte verfügbar", + "defi.nothing_supplied_yet": "Noch nichts geliefert", + "defi.oracle_price": "Orakelpreis", + "defi.platform_bonus": "Plattformbonus", + "defi.refundable_fee": "Rückzahlbare Gebühr", + "defi.repay": "Zurückzahlen", + "defi.reserve_size": "Reservegröße", + "defi.safe_max": "Sicherer Maximalwert", + "defi.select_an_asset_to_borrow": "Wählen Sie einen Vermögenswert aus, den Sie ausleihen möchten.", + "defi.select_an_asset_to_supply": "Wählen Sie ein zu lieferndes Asset", + "defi.show_assets_with_0_balance": "Assets mit 0 Guthaben anzeigen", + "defi.soft_liquidations": "Sanfte Liquidationen", + "defi.supplied": "Geliefert", + "defi.supplied_balance": "Gelieferter Saldo", + "defi.supply": "Angebot", + "defi.supply_apy": "Versorgungs-APY", + "defi.supply_assets_as_collateral_before_borrowing": "Vermögenswerte vor dem Ausleihen als Sicherheit bereitstellen", + "defi.supply_cap_usage": "Nutzung der Versorgungsobergrenze", + "defi.use_as_collateral": "Als Sicherheit verwenden", + "defi.utilization_ratio": "Auslastungsquote", + "defi.view_reserve_details": "Reserve-Details anzeigen", + "defi.with_collateral": "Mit Sicherheiten", + "defi.withdrawable_today": "Heute auszahlbar", "derivation_path": "Ableitungspfad", "description_403": "Unsere Dienstleistungen sind in Ihrer Region nicht verfügbar.", "device.btc_only_coming_soon": "Dieses Feature wird in einer späteren BTC-Only-Firmware-Version verfügbar sein.", @@ -1162,6 +1225,7 @@ "global.approvals": "Genehmigungen", "global.approve": "Genehmigen", "global.apr": "APR", + "global.apy": "APY", "global.asset": "Vermögen", "global.at_least_variable_characters": "Mindestens {variable} Zeichen", "global.auto": "Auto", @@ -1182,6 +1246,7 @@ "global.bluetooth": "Bluetooth", "global.bluetooth_firmware": "Bluetooth-Firmware", "global.bootloader": "Bootloader", + "global.borrow": "Ausleihen", "global.brightness": "Helligkeit", "global.browser": "Browser", "global.bulk": "Massen", @@ -1811,6 +1876,7 @@ "global.wallet": "Geldbörse", "global.wallet_activity": "Wallet-Aktivität", "global.wallet_avatar": "Geldbörsen-Avatar", + "global.wallet_balance": "Wallet-Guthaben", "global.wallet_history_notification_banner": "Aktiviere Benachrichtigungen, um sofortige Updates zu deinen Wallet-Aktivitäten zu erhalten.", "global.wallets": "Geldbörsen", "global.wallpaper": "Hintergrundbild", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "Authentifizierung fehlgeschlagen, überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut", "ln.payment_received_label": "Zahlung erhalten", "log_out_confirmation_text": "Möchten Sie sich wirklich von {email} abmelden?", + "log_out_wallet": "Wallet abmelden", + "log_out_wallet_desc": "Dadurch wird die Wallet von diesem Gerät entfernt. Du kannst dich jederzeit wieder mit deinem Social-Media-Konto und deiner PIN anmelden.", "logged_out_feedback": "Erfolgreich abgemeldet", "login.forgot_passcode": "Passcode vergessen?", "login.forgot_password": "Passwort vergessen?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "24H txns", "market.24h_vol_usd": "24H VOL(USD)", "market.30d": "30T", + "market.3m": "3M", "market.7d": "7T", "market.add_number_tokens": "Füge {number} Tokens hinzu", "market.add_to_favorites": "Zu Favoriten hinzufügen", @@ -3153,8 +3222,11 @@ "send.password_validation": "Das Passwort muss zwischen 8 und 128 Zeichen lang sein.", "send.preview_button": "Vorschau", "send.recipient_invalid": "Ungültiger Empfänger. Bitte überprüfen und erneut eingeben", + "send.recipient_token_not_activated": "Das Empfängerkonto hat dieses Token nicht aktiviert. Bitte bitten Sie den Empfänger, ein Token hinzuzufügen", "send.send_to_this_address": "An diese Adresse senden", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "Das Senden von {0} erfordert ein Kontoguthaben von mindestens {1} {2}.", + "send.stellar_activation_minimum_hint": "Ein Minimum von 1 XLM ist erforderlich, um das Konto zu aktivieren.", + "send.stellar_recipient_account_not_activated": "Das Empfängerkonto ist im Stellar-Netzwerk nicht aktiviert. Bitte senden Sie mindestens 1 XLM an diese Adresse, um es zu initialisieren.", "send.str_minimum_balance_is_str": "Senden fehlgeschlagen. Mindestguthaben für {token} beträgt {amount}.", "send.str_minimum_transfer": "Der Mindestüberweisungsbetrag beträgt {0}.", "send.suggest_reserving_str_as_gas_fee": "Es wird empfohlen, {0} als Netzwerkgebühr zu reservieren.", diff --git a/packages/shared/src/locale/json/en_US.json b/packages/shared/src/locale/json/en_US.json index 65a2d4f82fdd..c41c17483eb2 100644 --- a/packages/shared/src/locale/json/en_US.json +++ b/packages/shared/src/locale/json/en_US.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "Filled", "Limit.order_status_open": "Open", "Limit.order_status_unfilled": "Unfilled", + "Perps.BBO_Counterparty": "Counterparty 1", + "Perps.BBO_Queue": "Queue 1", + "Perps.BBO_button_desc": "Best-bid-offer (BBO) places a limit order at the best bid or ask price. If you use Post-Only, your order will only be placed if it adds liquidity. Due to market fluctuations, the order may fail to place if the market moves before it's confirmed.", + "Perps.BBO_button_title": "BBO", + "Perps.BBO_select_title": "Select BBO Mode", + "Perps.BBO_unavailable": "BBO Unavailable", "Perps.referral_bonus_from": "Referral bonus from trading on Perps", "account_model.watched": "Watch-Only", "account_name_form_helper_text": "Do not enter sensitive information.", @@ -452,9 +458,66 @@ "date.today": "Today", "date.yesterday": "Yesterday", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "Asset / Borrowed", + "defi.asset_can_be_collateral": "Asset / Can Be Collateral", + "defi.asset_supplied": "Asset / Supplied", + "defi.assets_to_borrow": "Assets to Borrow", + "defi.assets_to_supply": "Assets to Supply", + "defi.available_liquidity": "Available liquidity", + "defi.available_to_borrow": "Available to borrow", + "defi.borrow_apy": "Borrow APY", + "defi.borrow_cap_usage": "Borrow cap usage", + "defi.borrowable": "Borrowable", + "defi.borrowable_today": "Borrowable today", + "defi.borrowed": "Borrowed", + "defi.borrowed_balance": "Borrowed balance", + "defi.can_be_collateral": "Can be collateral", + "defi.claim_all": "Claim All", + "defi.claimable_rewards": "Claimable Rewards", + "defi.current_utilization": "Current utilization", + "defi.daily_borrow_cap": "Daily borrow cap", + "defi.daily_cap_resets_in": "Daily cap resets in", + "defi.daily_caps": "Daily caps", + "defi.daily_withdraw_cap": "Daily withdraw cap", + "defi.from_wallet_balance": "From Wallet Balance", + "defi.health_factor": "Health Factor", + "defi.interest_rate_model": "Interest rate model", "defi.liquidation_acknowledge": "I acknowledge the risks involved", + "defi.liquidation_at_less_than_1_00": "Liquidation at < 1.00", "defi.liquidation_borrow_desc": "Borrowing this amount will reduce your health factor and increase risk of liquidation.", + "defi.liquidation_ltv": "Liquidation LTV", "defi.liquidation_withdraw_desc": "Withdrawing this amount will reduce your health factor and increase risk of liquidation.", + "defi.manage_position": "Manage Position", + "defi.max_ltv": "Max LTV", + "defi.my_borrow": "My Borrow", + "defi.my_info": "My info", + "defi.my_supply": "My Supply", + "defi.net_apy": "Net APY", + "defi.net_worth": "Net Worth", + "defi.no_assets_to_borrow": "No assets available to borrow", + "defi.no_assets_to_supply": "No assets available to supply", + "defi.nothing_supplied_yet": "Nothing Supplied Yet", + "defi.oracle_price": "Oracle price", + "defi.platform_bonus": "Platform Bonus", + "defi.refundable_fee": "Refundable Fee", + "defi.repay": "Repay", + "defi.reserve_size": "Reserve size", + "defi.safe_max": "Safe Max", + "defi.select_an_asset_to_borrow": "Select an asset to borrow", + "defi.select_an_asset_to_supply": "Select an asset to supply", + "defi.show_assets_with_0_balance": "Show Assets with 0 Balance", + "defi.soft_liquidations": "Soft Liquidations", + "defi.supplied": "Supplied", + "defi.supplied_balance": "Supplied balance", + "defi.supply": "Supply", + "defi.supply_apy": "Supply APY", + "defi.supply_assets_as_collateral_before_borrowing": "Supply Assets as Collateral Before Borrowing", + "defi.supply_cap_usage": "Supply cap usage", + "defi.use_as_collateral": "Use as Collateral", + "defi.utilization_ratio": "Utilization ratio", + "defi.view_reserve_details": "View Reserve Details", + "defi.with_collateral": "With Collateral", + "defi.withdrawable_today": "Withdrawable today", "derivation_path": "Derivation path", "description_403": "Our services are not available in your region.", "device.btc_only_coming_soon": "This feature will be available in a later BTC-Only firmware release.", @@ -1162,6 +1225,7 @@ "global.approvals": "Approvals", "global.approve": "Approve", "global.apr": "APR", + "global.apy": "APY", "global.asset": "Asset", "global.at_least_variable_characters": "At least {variable} characters", "global.auto": "Auto", @@ -1182,6 +1246,7 @@ "global.bluetooth": "Bluetooth", "global.bluetooth_firmware": "Bluetooth firmware", "global.bootloader": "Bootloader", + "global.borrow": "Borrow", "global.brightness": "Brightness", "global.browser": "Browser", "global.bulk": "Bulk", @@ -1811,6 +1876,7 @@ "global.wallet": "Wallet", "global.wallet_activity": "Wallet activity", "global.wallet_avatar": "Wallet avatar", + "global.wallet_balance": "Wallet balance", "global.wallet_history_notification_banner": "Enable notifications to get instant updates on your wallet activity.", "global.wallets": "Wallets", "global.wallpaper": "Wallpaper", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "Authentication failed, check your network connection and try again", "ln.payment_received_label": "Payment received", "log_out_confirmation_text": "Are you sure you want to log out of {email}?", + "log_out_wallet": "Log out wallet", + "log_out_wallet_desc": "This will remove the wallet from this device. You can log back in anytime using your social account and PIN.", "logged_out_feedback": "Logged out successfully", "login.forgot_passcode": "Forgot passcode?", "login.forgot_password": "Forgot password?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "24H txns", "market.24h_vol_usd": "24H VOL(USD)", "market.30d": "30D", + "market.3m": "3M", "market.7d": "7D", "market.add_number_tokens": "Add {number} tokens", "market.add_to_favorites": "Add to favorites", @@ -3153,8 +3222,11 @@ "send.password_validation": "Password must be between 8 and 128 characters.", "send.preview_button": "Preview", "send.recipient_invalid": "Invalid recipient. Please check and re-enter", + "send.recipient_token_not_activated": "The recipient account has not activated this token. Please ask the recipient to add a Token", "send.send_to_this_address": "Send to this address", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "Sending {0} requires an account balance of at least {1} {2}.", + "send.stellar_activation_minimum_hint": "A minimum of 1 XLM is required to activate the account.", + "send.stellar_recipient_account_not_activated": "The recipient account is not activated on the Stellar network. Please send at least 1 XLM to this address to initialize it.", "send.str_minimum_balance_is_str": "Sending failed. Minimum balance for {token} is {amount}.", "send.str_minimum_transfer": "Minimum transfer amount is {0}.", "send.suggest_reserving_str_as_gas_fee": "Suggest reserving {0} as a network fee.", diff --git a/packages/shared/src/locale/json/es.json b/packages/shared/src/locale/json/es.json index 05f252b7d188..27a6f37d7414 100644 --- a/packages/shared/src/locale/json/es.json +++ b/packages/shared/src/locale/json/es.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "Lleno", "Limit.order_status_open": "Abrir", "Limit.order_status_unfilled": "No completado", + "Perps.BBO_Counterparty": "Contraparte 1", + "Perps.BBO_Queue": "Cola 1", + "Perps.BBO_button_desc": "La mejor oferta y demanda (BBO) coloca una orden limitada al mejor precio de compra o de venta. Si utilizas Solo publicación (Post-Only), tu orden solo se enviará si añade liquidez. Debido a las fluctuaciones del mercado, la orden puede no llegar a colocarse si el mercado se mueve antes de que se confirme.", + "Perps.BBO_button_title": "BBO", + "Perps.BBO_select_title": "Seleccionar modo de mejor precio", + "Perps.BBO_unavailable": "Mejor oferta disponible no disponible", "Perps.referral_bonus_from": "Bono por referidos de operaciones en Perps", "account_model.watched": "Visto", "account_name_form_helper_text": "No introduzcas información sensible.", @@ -452,9 +458,66 @@ "date.today": "Hoy", "date.yesterday": "Ayer", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "Activo / Prestado", + "defi.asset_can_be_collateral": "Activo / Puede ser Garantía", + "defi.asset_supplied": "Activo / Suministrado", + "defi.assets_to_borrow": "Activos para Pedir Prestado", + "defi.assets_to_supply": "Activos para Suministrar", + "defi.available_liquidity": "Liquidez disponible", + "defi.available_to_borrow": "Disponible para prestar", + "defi.borrow_apy": "APY de préstamo", + "defi.borrow_cap_usage": "Uso del límite de préstamo", + "defi.borrowable": "Prestado", + "defi.borrowable_today": "Prestado hoy", + "defi.borrowed": "Prestado", + "defi.borrowed_balance": "Saldo prestado", + "defi.can_be_collateral": "Puede ser garantía", + "defi.claim_all": "Reclamar Todo", + "defi.claimable_rewards": "Recompensas Reclamables", + "defi.current_utilization": "Utilización actual", + "defi.daily_borrow_cap": "Límite diario de préstamo", + "defi.daily_cap_resets_in": "El límite diario se restablece en", + "defi.daily_caps": "Límites diarios", + "defi.daily_withdraw_cap": "Límite diario de retiro", + "defi.from_wallet_balance": "Desde el Saldo de la Billetera", + "defi.health_factor": "Factor de Salud", + "defi.interest_rate_model": "Modelo de tasa de interés", "defi.liquidation_acknowledge": "Reconozco los riesgos que conlleva", + "defi.liquidation_at_less_than_1_00": "Liquidación en < 1.00", "defi.liquidation_borrow_desc": "Pedir prestada esta cantidad reducirá su factor de salud y aumentará el riesgo de liquidación.", + "defi.liquidation_ltv": "LTV de liquidación", "defi.liquidation_withdraw_desc": "Retirar esta cantidad reducirá su factor de salud y aumentará el riesgo de liquidación.", + "defi.manage_position": "Gestionar posición", + "defi.max_ltv": "LTV Máximo", + "defi.my_borrow": "Mi Préstamo", + "defi.my_info": "Mi información", + "defi.my_supply": "Mi Suministro", + "defi.net_apy": "APY Neto", + "defi.net_worth": "Valor Neto", + "defi.no_assets_to_borrow": "No hay activos disponibles para pedir prestado", + "defi.no_assets_to_supply": "No hay activos disponibles para suministrar", + "defi.nothing_supplied_yet": "Nada suministrado todavía", + "defi.oracle_price": "Precio del oráculo", + "defi.platform_bonus": "Bono de Plataforma", + "defi.refundable_fee": "Tarifa Reembolsable", + "defi.repay": "Pagar", + "defi.reserve_size": "Tamaño de la reserva", + "defi.safe_max": "Máximo Seguro", + "defi.select_an_asset_to_borrow": "Seleccione un activo para pedir prestado", + "defi.select_an_asset_to_supply": "Seleccione un activo para suministrar", + "defi.show_assets_with_0_balance": "Mostrar Activos con Saldo 0", + "defi.soft_liquidations": "Liquidaciones suaves", + "defi.supplied": "Suministrado", + "defi.supplied_balance": "Saldo suministrado", + "defi.supply": "Suministro", + "defi.supply_apy": "APY de suministro", + "defi.supply_assets_as_collateral_before_borrowing": "Suministrar activos como garantía antes de pedir prestado", + "defi.supply_cap_usage": "Uso del límite de suministro", + "defi.use_as_collateral": "Usar como Garantía", + "defi.utilization_ratio": "Tasa de utilización", + "defi.view_reserve_details": "Ver Detalles de la Reserva", + "defi.with_collateral": "Con Garantía", + "defi.withdrawable_today": "Retirable hoy", "derivation_path": "Ruta de derivación", "description_403": "Nuestros servicios no están disponibles en su región.", "device.btc_only_coming_soon": "Esta función estará disponible en una futura versión de firmware Solo-BTC.", @@ -1162,6 +1225,7 @@ "global.approvals": "Aprobaciones", "global.approve": "Aprobar", "global.apr": "APR", + "global.apy": "APY", "global.asset": "Activo", "global.at_least_variable_characters": "Al menos {variable} caracteres", "global.auto": "Auto", @@ -1182,6 +1246,7 @@ "global.bluetooth": "Bluetooth", "global.bluetooth_firmware": "Firmware de Bluetooth", "global.bootloader": "Bootloader", + "global.borrow": "Pedir prestado", "global.brightness": "Brillo", "global.browser": "Navegador", "global.bulk": "Masivo", @@ -1811,6 +1876,7 @@ "global.wallet": "Cartera", "global.wallet_activity": "Actividad de la billetera", "global.wallet_avatar": "Avatar de billetera", + "global.wallet_balance": "Saldo de la billetera", "global.wallet_history_notification_banner": "Activa las notificaciones para recibir actualizaciones instantáneas sobre la actividad de tu billetera.", "global.wallets": "Carteras", "global.wallpaper": "Fondo de pantalla", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "La autenticación falló, verifica tu conexión a la red e intenta de nuevo", "ln.payment_received_label": "Pago recibido", "log_out_confirmation_text": "¿Estás seguro de que quieres cerrar sesión de {email}?", + "log_out_wallet": "Cerrar sesión en la billetera", + "log_out_wallet_desc": "Esto eliminará la billetera de este dispositivo. Puedes volver a iniciar sesión en cualquier momento usando tu cuenta social y tu PIN.", "logged_out_feedback": "Cierre de sesión exitoso", "login.forgot_passcode": "¿Olvidaste el código?", "login.forgot_password": "¿Olvidó su contraseña?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "Transacciones 24H", "market.24h_vol_usd": "VOL 24H(USD)", "market.30d": "30D", + "market.3m": "3M", "market.7d": "7D", "market.add_number_tokens": "Añadir {number} tokens", "market.add_to_favorites": "Agregar a favoritos", @@ -3153,8 +3222,11 @@ "send.password_validation": "La contraseña debe tener entre 8 y 128 caracteres.", "send.preview_button": "Vista previa", "send.recipient_invalid": "Destinatario no válido. Por favor, verifica y vuelve a introducir", + "send.recipient_token_not_activated": "La cuenta del destinatario no ha activado este token. Por favor, pida al destinatario que añada un Token", "send.send_to_this_address": "Enviar a esta dirección", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "Enviar {0} requiere un saldo en la cuenta de al menos {1} {2}.", + "send.stellar_activation_minimum_hint": "Se requiere un mínimo de 1 XLM para activar la cuenta.", + "send.stellar_recipient_account_not_activated": "La cuenta del destinatario no está activada en la red Stellar. Por favor, envíe al menos 1 XLM a esta dirección para inicializarla.", "send.str_minimum_balance_is_str": "El envío ha fallado. El saldo mínimo para {token} es {amount}.", "send.str_minimum_transfer": "El monto mínimo de transferencia es {0}.", "send.suggest_reserving_str_as_gas_fee": "Sugiere reservar {0} como tarifa de red.", diff --git a/packages/shared/src/locale/json/fr_FR.json b/packages/shared/src/locale/json/fr_FR.json index 7de351865437..0e851de3e980 100644 --- a/packages/shared/src/locale/json/fr_FR.json +++ b/packages/shared/src/locale/json/fr_FR.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "Rempli", "Limit.order_status_open": "Ouvrir", "Limit.order_status_unfilled": "Non rempli", + "Perps.BBO_Counterparty": "Contrepartie 1", + "Perps.BBO_Queue": "File 1", + "Perps.BBO_button_desc": "L'offre au meilleur prix (BBO) place un ordre à cours limité au meilleur prix acheteur ou vendeur. Si vous utilisez Post-Only, votre ordre ne sera placé que s'il ajoute de la liquidité. En raison des fluctuations du marché, l'ordre peut ne pas être placé si le marché évolue avant sa confirmation.", + "Perps.BBO_button_title": "BBO", + "Perps.BBO_select_title": "Sélectionner le mode BBO", + "Perps.BBO_unavailable": "BBO indisponible", "Perps.referral_bonus_from": "Bonus de parrainage provenant du trading sur Perps", "account_model.watched": "Regardé", "account_name_form_helper_text": "Ne saisissez pas d'informations sensibles.", @@ -452,9 +458,66 @@ "date.today": "Aujourd'hui", "date.yesterday": "Hier", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "Actif / Emprunté", + "defi.asset_can_be_collateral": "Actif / Peut être une garantie", + "defi.asset_supplied": "Actif / Fourni", + "defi.assets_to_borrow": "Actifs à emprunter", + "defi.assets_to_supply": "Actifs à fournir", + "defi.available_liquidity": "Liquidité disponible", + "defi.available_to_borrow": "Disponible à l'emprunt", + "defi.borrow_apy": "APY d'emprunt", + "defi.borrow_cap_usage": "Utilisation du plafond d'emprunt", + "defi.borrowable": "Empruntable", + "defi.borrowable_today": "Empruntable aujourd'hui", + "defi.borrowed": "Emprunté", + "defi.borrowed_balance": "Solde emprunté", + "defi.can_be_collateral": "Peut être une garantie", + "defi.claim_all": "Réclamer tout", + "defi.claimable_rewards": "Récompenses à réclamer", + "defi.current_utilization": "Utilisation actuelle", + "defi.daily_borrow_cap": "Plafon d'emprunt quotidien", + "defi.daily_cap_resets_in": "Le plafond quotidien se réinitialise dans", + "defi.daily_caps": "Plafonds quotidiens", + "defi.daily_withdraw_cap": "Plafon de retrait quotidien", + "defi.from_wallet_balance": "Depuis le solde du portefeuille", + "defi.health_factor": "Facteur de santé", + "defi.interest_rate_model": "Modèle de taux d'intérêt", "defi.liquidation_acknowledge": "Je reconnais les risques encourus.", + "defi.liquidation_at_less_than_1_00": "Liquidation à < 1.00", "defi.liquidation_borrow_desc": "Emprunter cette somme réduira votre facteur de santé et augmentera le risque de liquidation.", + "defi.liquidation_ltv": "LTV de liquidation", "defi.liquidation_withdraw_desc": "Retirer cette somme réduira votre facteur de santé et augmentera le risque de liquidation.", + "defi.manage_position": "Gérer la position", + "defi.max_ltv": "LTV max", + "defi.my_borrow": "Mon emprunt", + "defi.my_info": "Mes infos", + "defi.my_supply": "Mon approvisionnement", + "defi.net_apy": "APY net", + "defi.net_worth": "Valeur nette", + "defi.no_assets_to_borrow": "Aucun actif disponible pour emprunter", + "defi.no_assets_to_supply": "Aucun actif disponible pour l'approvisionnement", + "defi.nothing_supplied_yet": "Rien n'a encore été fourni", + "defi.oracle_price": "Prix de l'oracle", + "defi.platform_bonus": "Bonus de plateforme", + "defi.refundable_fee": "Frais remboursables", + "defi.repay": "Rembourser", + "defi.reserve_size": "Taille de la réserve", + "defi.safe_max": "Maximum sécurisé", + "defi.select_an_asset_to_borrow": "Choisissez un actif à emprunter", + "defi.select_an_asset_to_supply": "Sélectionnez un actif à fournir", + "defi.show_assets_with_0_balance": "Afficher les actifs avec 0 solde", + "defi.soft_liquidations": "Liquidations souples", + "defi.supplied": "Fourni", + "defi.supplied_balance": "Solde fourni", + "defi.supply": "Fournir", + "defi.supply_apy": "APY d'approvisionnement", + "defi.supply_assets_as_collateral_before_borrowing": "Fournir des actifs en garantie avant d'emprunter", + "defi.supply_cap_usage": "Utilisation du plafond d'approvisionnement", + "defi.use_as_collateral": "Utiliser comme garantie", + "defi.utilization_ratio": "Taux d'utilisation", + "defi.view_reserve_details": "Afficher les détails de la réserve", + "defi.with_collateral": "Avec garantie", + "defi.withdrawable_today": "Retirable aujourd'hui", "derivation_path": "Chemin de dérivation", "description_403": "Nos services ne sont pas disponibles dans votre région.", "device.btc_only_coming_soon": "Cette fonctionnalité sera disponible dans une version ultérieure du micrologiciel BTC-Only.", @@ -1162,6 +1225,7 @@ "global.approvals": "Approbations", "global.approve": "Approuver", "global.apr": "APR", + "global.apy": "APY", "global.asset": "Actif", "global.at_least_variable_characters": "Au moins {variable} caractères", "global.auto": "Auto", @@ -1182,6 +1246,7 @@ "global.bluetooth": "Bluetooth", "global.bluetooth_firmware": "Micrologiciel Bluetooth", "global.bootloader": "Bootloader", + "global.borrow": "Emprunter", "global.brightness": "Luminosité", "global.browser": "Navigateur", "global.bulk": "En masse", @@ -1811,6 +1876,7 @@ "global.wallet": "Portefeuille", "global.wallet_activity": "Activité du portefeuille", "global.wallet_avatar": "Avatar de portefeuille", + "global.wallet_balance": "Solde du portefeuille", "global.wallet_history_notification_banner": "Activez les notifications pour recevoir des mises à jour instantanées sur l'activité de votre portefeuille.", "global.wallets": "Portefeuilles", "global.wallpaper": "Fond d'écran", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "L'authentification a échoué, vérifiez votre connexion réseau et essayez à nouveau", "ln.payment_received_label": "Paiement reçu", "log_out_confirmation_text": "Voulez-vous vraiment vous déconnecter de {email} ?", + "log_out_wallet": "Déconnecter le portefeuille", + "log_out_wallet_desc": "Cela supprimera le portefeuille de cet appareil. Vous pouvez vous reconnecter à tout moment en utilisant votre compte social et votre code PIN.", "logged_out_feedback": "Déconnexion réussie", "login.forgot_passcode": "Mot de passe oublié ?", "login.forgot_password": "Mot de passe oublié ?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "24H txns", "market.24h_vol_usd": "VOL 24H(USD)", "market.30d": "30J", + "market.3m": "3M", "market.7d": "7J", "market.add_number_tokens": "Ajouter {number} jetons", "market.add_to_favorites": "Ajouter aux favoris", @@ -3153,8 +3222,11 @@ "send.password_validation": "Le mot de passe doit comporter entre 8 et 128 caractères.", "send.preview_button": "Aperçu", "send.recipient_invalid": "Destinataire invalide. Veuillez vérifier et saisir à nouveau", + "send.recipient_token_not_activated": "Le compte du destinataire n'a pas activé ce jeton. Veuillez demander au destinataire d'ajouter un jeton", "send.send_to_this_address": "Envoyer à cette adresse", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "L'envoi de {0} nécessite un solde de compte d'au moins {1} {2}.", + "send.stellar_activation_minimum_hint": "Un minimum de 1 XLM est requis pour activer le compte.", + "send.stellar_recipient_account_not_activated": "Le compte du destinataire n'est pas activé sur le réseau Stellar. Veuillez envoyer au moins 1 XLM à cette adresse pour l'initialiser.", "send.str_minimum_balance_is_str": "L'envoi a échoué. Le solde minimum pour {token} est de {amount}.", "send.str_minimum_transfer": "Le montant minimum de transfert est de {0}.", "send.suggest_reserving_str_as_gas_fee": "Suggérez de réserver {0} comme frais de réseau.", diff --git a/packages/shared/src/locale/json/hi_IN.json b/packages/shared/src/locale/json/hi_IN.json index 44b1db9716cf..6a645d08b0e2 100644 --- a/packages/shared/src/locale/json/hi_IN.json +++ b/packages/shared/src/locale/json/hi_IN.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "भरा हुआ", "Limit.order_status_open": "खोलें", "Limit.order_status_unfilled": "अपूर्ण", + "Perps.BBO_Counterparty": "प्रतिपक्ष 1", + "Perps.BBO_Queue": "कतार 1", + "Perps.BBO_button_desc": "बेस्ट-बिड-ऑफर (BBO) सर्वोत्तम बिड या आस्क प्राइस पर एक लिमिट ऑर्डर लगाता है। यदि आप पोस्ट-ओनली का उपयोग करते हैं, तो आपका ऑर्डर केवल तभी लगाया जाएगा जब यह लिक्विडिटी जोड़ता हो। बाजार में उतार-चढ़ाव के कारण, यदि ऑर्डर की पुष्टि से पहले बाजार में बदलाव होता है तो ऑर्डर लगाने में विफलता हो सकती है।", + "Perps.BBO_button_title": "BBO", + "Perps.BBO_select_title": "BBO मोड चुनें", + "Perps.BBO_unavailable": "BBO अनुपलब्ध", "Perps.referral_bonus_from": "Perps पर ट्रेडिंग से रेफरल बोनस", "account_model.watched": "देखा", "account_name_form_helper_text": "संवेदनशील जानकारी दर्ज न करें।", @@ -452,9 +458,66 @@ "date.today": "आज", "date.yesterday": "कल", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "संपत्ति / उधार लिया हुआ", + "defi.asset_can_be_collateral": "संपत्ति / संपार्श्विक हो सकता है", + "defi.asset_supplied": "संपत्ति / आपूर्ति की गई", + "defi.assets_to_borrow": "उधार लेने के लिए संपत्ति", + "defi.assets_to_supply": "आपूर्ति के लिए संपत्ति", + "defi.available_liquidity": "उपलब्ध तरलता", + "defi.available_to_borrow": "उधार लेने के लिए उपलब्ध", + "defi.borrow_apy": "उधार APY", + "defi.borrow_cap_usage": "उधार सीमा उपयोग", + "defi.borrowable": "उधार लेने योग्य", + "defi.borrowable_today": "आज उधार लेने योग्य", + "defi.borrowed": "उधार लिया हुआ", + "defi.borrowed_balance": "उधार लिया गया शेष", + "defi.can_be_collateral": "संपार्श्विक हो सकता है", + "defi.claim_all": "सभी का दावा करें", + "defi.claimable_rewards": "दावा योग्य पुरस्कार", + "defi.current_utilization": "वर्तमान उपयोग", + "defi.daily_borrow_cap": "दैनिक उधार सीमा", + "defi.daily_cap_resets_in": "दैनिक सीमा रीसेट होती है", + "defi.daily_caps": "दैनिक सीमाएं", + "defi.daily_withdraw_cap": "दैनिक निकासी सीमा", + "defi.from_wallet_balance": "वॉलेट बैलेंस से", + "defi.health_factor": "स्वास्थ्य कारक", + "defi.interest_rate_model": "ब्याज दर मॉडल", "defi.liquidation_acknowledge": "मैं इसमें शामिल जोखिमों को स्वीकार करता हूँ।", + "defi.liquidation_at_less_than_1_00": "< 1.00 पर परिसमापन", "defi.liquidation_borrow_desc": "इतनी राशि उधार लेने से आपका स्वास्थ्य कारक कम हो जाएगा और दिवालियापन का जोखिम बढ़ जाएगा।", + "defi.liquidation_ltv": "परिसमापन एलटीवी", "defi.liquidation_withdraw_desc": "इस राशि को निकालने से आपका स्वास्थ्य कारक कम हो जाएगा और दिवालियापन का जोखिम बढ़ जाएगा।", + "defi.manage_position": "स्थिति प्रबंधित करें", + "defi.max_ltv": "अधिकतम एलटीवी", + "defi.my_borrow": "मेरा उधार", + "defi.my_info": "मेरी जानकारी", + "defi.my_supply": "मेरी आपूर्ति", + "defi.net_apy": "नेट एपीवाई", + "defi.net_worth": "कुल संपत्ति", + "defi.no_assets_to_borrow": "उधार लेने के लिए कोई संपत्ति उपलब्ध नहीं है", + "defi.no_assets_to_supply": "आपूर्ति के लिए कोई संपत्ति उपलब्ध नहीं है", + "defi.nothing_supplied_yet": "अभी तक कुछ भी आपूर्ति नहीं की गई है", + "defi.oracle_price": "ओरेकल मूल्य", + "defi.platform_bonus": "प्लेटफ़ॉर्म बोनस", + "defi.refundable_fee": "वापसी योग्य शुल्क", + "defi.repay": "चुकाना", + "defi.reserve_size": "आरक्षित आकार", + "defi.safe_max": "सुरक्षित अधिकतम", + "defi.select_an_asset_to_borrow": "उधार लेने के लिए एक परिसंपत्ति का चयन करें", + "defi.select_an_asset_to_supply": "आपूर्ति के लिए एक संपत्ति का चयन करें", + "defi.show_assets_with_0_balance": "0 शेष के साथ संपत्ति दिखाएं", + "defi.soft_liquidations": "नरम परिसमापन", + "defi.supplied": "आपूर्ति की गई", + "defi.supplied_balance": "प्रदत्त शेष", + "defi.supply": "आपूर्ति", + "defi.supply_apy": "आपूर्ति APY", + "defi.supply_assets_as_collateral_before_borrowing": "उधार लेने से पहले संपत्ति को संपार्श्विक के रूप में आपूर्ति करें", + "defi.supply_cap_usage": "आपूर्ति सीमा का उपयोग", + "defi.use_as_collateral": "संपार्श्विक के रूप में उपयोग करें", + "defi.utilization_ratio": "उपयोगिता अनुपात", + "defi.view_reserve_details": "रिजर्व विवरण देखें", + "defi.with_collateral": "संपार्श्विक के साथ", + "defi.withdrawable_today": "आज निकालने योग्य", "derivation_path": "व्युत्पन्न पथ", "description_403": "हमारी सेवाएं आपके क्षेत्र में उपलब्ध नहीं हैं।", "device.btc_only_coming_soon": "यह सुविधा बाद में BTC-Only फर्मवेयर रिलीज़ में उपलब्ध होगी।", @@ -1162,6 +1225,7 @@ "global.approvals": "मंज़ूरी", "global.approve": "स्वीकार करें", "global.apr": "APR", + "global.apy": "APY", "global.asset": "संपत्ति", "global.at_least_variable_characters": "कम से कम {variable} वर्ण", "global.auto": "ऑटो", @@ -1182,6 +1246,7 @@ "global.bluetooth": "ब्लूटूथ", "global.bluetooth_firmware": "ब्लूटूथ फर्मवेयर", "global.bootloader": "Bootloader", + "global.borrow": "उधार लें", "global.brightness": "चमक", "global.browser": "ब्राउज़र", "global.bulk": "थोक", @@ -1811,6 +1876,7 @@ "global.wallet": "वॉलेट", "global.wallet_activity": "वॉलेट गतिविधि", "global.wallet_avatar": "वॉलेट अवतार", + "global.wallet_balance": "वॉलेट बैलेंस", "global.wallet_history_notification_banner": "अपने वॉलेट की गतिविधि पर तुरंत अपडेट पाने के लिए सूचनाएं सक्षम करें।", "global.wallets": "वॉलेट्स", "global.wallpaper": "वॉलपेपर", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "प्रमाणीकरण विफल रहा, कृपया अपने नेटवर्क कनेक्शन की जांच करें और पुनः प्रयास करें", "ln.payment_received_label": "भुगतान प्राप्त", "log_out_confirmation_text": "क्या आप वाकई {email} से लॉग आउट करना चाहते हैं?", + "log_out_wallet": "वॉलेट से लॉग आउट करें", + "log_out_wallet_desc": "यह इस डिवाइस से वॉलेट को हटा देगा। आप अपने सोशल अकाउंट और PIN का उपयोग करके किसी भी समय वापस लॉग इन कर सकते हैं।", "logged_out_feedback": "सफलतापूर्वक लॉग आउट किया गया", "login.forgot_passcode": "पासकोड भूल गए?", "login.forgot_password": "पासवर्ड भूल गए?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "24 घंटे के लेनदेन", "market.24h_vol_usd": "24 घंटे का वॉल्यूम (USD)", "market.30d": "30 दिन", + "market.3m": "3 महीना", "market.7d": "7 दिन", "market.add_number_tokens": "{number} टोकन जोड़ें", "market.add_to_favorites": "पसंदीदा में जोड़ें", @@ -3153,8 +3222,11 @@ "send.password_validation": "पासवर्ड 8 से 128 वर्णों के बीच होना चाहिए।", "send.preview_button": "पूर्वावलोकन", "send.recipient_invalid": "अमान्य प्राप्तकर्ता। कृपया जांचें और पुनः दर्ज करें", + "send.recipient_token_not_activated": "प्राप्तकर्ता खाते ने इस टोकन को सक्रिय नहीं किया है। कृपया प्राप्तकर्ता से टोकन जोड़ने का अनुरोध करें।", "send.send_to_this_address": "इस पते पर भेजें", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "{0} भेजने के लिए कम से कम {1} {2} का खाता शेष होना चाहिए।", + "send.stellar_activation_minimum_hint": "खाता सक्रिय करने के लिए कम से कम 1 एक्सएलएम की आवश्यकता होती है।", + "send.stellar_recipient_account_not_activated": "प्राप्तकर्ता का खाता स्टेलर नेटवर्क पर सक्रिय नहीं है। कृपया इसे सक्रिय करने के लिए इस पते पर कम से कम 1 XLM फ़ाइल भेजें।", "send.str_minimum_balance_is_str": "भेजना विफल रहा। {token} के लिए न्यूनतम शेष राशि {amount} है।", "send.str_minimum_transfer": "न्यूनतम हस्तांतरण राशि {0} है।", "send.suggest_reserving_str_as_gas_fee": "{0} को नेटवर्क शुल्क के रूप में आरक्षित करने का सुझाव दें।", diff --git a/packages/shared/src/locale/json/id.json b/packages/shared/src/locale/json/id.json index 3cc7162be74f..9e61cd21199e 100644 --- a/packages/shared/src/locale/json/id.json +++ b/packages/shared/src/locale/json/id.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "Terisi", "Limit.order_status_open": "Buka", "Limit.order_status_unfilled": "Tidak Terisi", + "Perps.BBO_Counterparty": "Pihak Lawan 1", + "Perps.BBO_Queue": "Antrian 1", + "Perps.BBO_button_desc": "Best-bid-offer (BBO) menempatkan limit order pada harga bid atau ask terbaik. Jika Anda menggunakan Post-Only, order Anda hanya akan ditempatkan jika menambah likuiditas. Karena fluktuasi pasar, order mungkin gagal ditempatkan jika pasar bergerak sebelum dikonfirmasi.", + "Perps.BBO_button_title": "BBO", + "Perps.BBO_select_title": "Pilih Mode BBO", + "Perps.BBO_unavailable": "BBO Tidak Tersedia", "Perps.referral_bonus_from": "Bonus referral dari trading di Perps", "account_model.watched": "Ditonton", "account_name_form_helper_text": "Jangan masukkan informasi sensitif.", @@ -452,9 +458,66 @@ "date.today": "Hari ini", "date.yesterday": "Kemarin", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "Aset / Dipinjam", + "defi.asset_can_be_collateral": "Aset / Dapat Menjadi Jaminan", + "defi.asset_supplied": "Aset / Disuplai", + "defi.assets_to_borrow": "Aset untuk Dipinjam", + "defi.assets_to_supply": "Aset untuk Disuplai", + "defi.available_liquidity": "Likuiditas tersedia", + "defi.available_to_borrow": "Tersedia untuk dipinjam", + "defi.borrow_apy": "APY pinjaman", + "defi.borrow_cap_usage": "Penggunaan batas pinjaman", + "defi.borrowable": "Dapat dipinjam", + "defi.borrowable_today": "Dapat dipinjam hari ini", + "defi.borrowed": "Dipinjam", + "defi.borrowed_balance": "Saldo pinjaman", + "defi.can_be_collateral": "Dapat menjadi jaminan", + "defi.claim_all": "Klaim Semua", + "defi.claimable_rewards": "Hadiah yang Dapat Diklaim", + "defi.current_utilization": "Pemanfaatan saat ini", + "defi.daily_borrow_cap": "Batas pinjaman harian", + "defi.daily_cap_resets_in": "Batas harian direset dalam", + "defi.daily_caps": "Batas harian", + "defi.daily_withdraw_cap": "Batas penarikan harian", + "defi.from_wallet_balance": "Dari Saldo Dompet", + "defi.health_factor": "Faktor Kesehatan", + "defi.interest_rate_model": "Model suku bunga", "defi.liquidation_acknowledge": "Saya menyadari risiko yang terlibat.", + "defi.liquidation_at_less_than_1_00": "Likuidasi pada < 1.00", "defi.liquidation_borrow_desc": "Meminjam sejumlah uang ini akan mengurangi faktor kesehatan Anda dan meningkatkan risiko likuidasi.", + "defi.liquidation_ltv": "LTV Likuidasi", "defi.liquidation_withdraw_desc": "Menarik dana sebesar ini akan mengurangi faktor kesehatan Anda dan meningkatkan risiko likuidasi.", + "defi.manage_position": "Kelola Posisi", + "defi.max_ltv": "LTV Maks", + "defi.my_borrow": "Pinjaman Saya", + "defi.my_info": "Info saya", + "defi.my_supply": "Pasokan Saya", + "defi.net_apy": "APY Bersih", + "defi.net_worth": "Kekayaan Bersih", + "defi.no_assets_to_borrow": "Tidak ada aset yang tersedia untuk dipinjam.", + "defi.no_assets_to_supply": "Tidak ada aset yang tersedia untuk memasok", + "defi.nothing_supplied_yet": "Belum ada yang dipasok", + "defi.oracle_price": "Harga Oracle", + "defi.platform_bonus": "Bonus Platform", + "defi.refundable_fee": "Biaya yang Dapat Dikembalikan", + "defi.repay": "Bayar Kembali", + "defi.reserve_size": "Ukuran cadangan", + "defi.safe_max": "Maksimal Aman", + "defi.select_an_asset_to_borrow": "Pilih aset yang ingin dipinjam", + "defi.select_an_asset_to_supply": "Pilih aset untuk dipasok", + "defi.show_assets_with_0_balance": "Tampilkan Aset dengan Saldo 0", + "defi.soft_liquidations": "Likuidasi Lunak", + "defi.supplied": "Disuplai", + "defi.supplied_balance": "Saldo yang disediakan", + "defi.supply": "Pasokan", + "defi.supply_apy": "APY pasokan", + "defi.supply_assets_as_collateral_before_borrowing": "Pasok aset sebagai jaminan sebelum meminjam", + "defi.supply_cap_usage": "Penggunaan batas pasokan", + "defi.use_as_collateral": "Gunakan sebagai Agunan", + "defi.utilization_ratio": "Rasio pemanfaatan", + "defi.view_reserve_details": "Lihat Detail Cadangan", + "defi.with_collateral": "Dengan Agunan", + "defi.withdrawable_today": "Dapat ditarik hari ini", "derivation_path": "Jalur derivasi", "description_403": "Layanan kami tidak tersedia di wilayah Anda.", "device.btc_only_coming_soon": "Fitur ini akan tersedia dalam rilis firmware BTC-Only selanjutnya.", @@ -1162,6 +1225,7 @@ "global.approvals": "Persetujuan", "global.approve": "Setujui", "global.apr": "APR", + "global.apy": "APY", "global.asset": "Aset", "global.at_least_variable_characters": "Setidaknya {variable} karakter", "global.auto": "Mobil", @@ -1182,6 +1246,7 @@ "global.bluetooth": "Bluetooth", "global.bluetooth_firmware": "Firmware Bluetooth", "global.bootloader": "Bootloader", + "global.borrow": "Pinjam", "global.brightness": "Kecerahan", "global.browser": "Peramban", "global.bulk": "Massal", @@ -1811,6 +1876,7 @@ "global.wallet": "Dompet", "global.wallet_activity": "Aktivitas dompet", "global.wallet_avatar": "Avatar dompet", + "global.wallet_balance": "Saldo dompet", "global.wallet_history_notification_banner": "Aktifkan notifikasi untuk mendapatkan pembaruan instan tentang aktivitas dompet Anda.", "global.wallets": "Dompet", "global.wallpaper": "Wallpaper", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "Otentikasi gagal, periksa koneksi jaringan Anda dan coba lagi", "ln.payment_received_label": "Pembayaran diterima", "log_out_confirmation_text": "Apakah Anda yakin ingin keluar dari {email}?", + "log_out_wallet": "Keluar dari dompet", + "log_out_wallet_desc": "Ini akan menghapus dompet dari perangkat ini. Anda dapat masuk kembali kapan saja menggunakan akun sosial dan PIN Anda.", "logged_out_feedback": "Berhasil keluar", "login.forgot_passcode": "Lupa kode sandi?", "login.forgot_password": "Lupa kata sandi?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "Transaksi 24 Jam", "market.24h_vol_usd": "Volume 24J (USD)", "market.30d": "30hr", + "market.3m": "3bln", "market.7d": "7hr", "market.add_number_tokens": "Tambahkan {number} token", "market.add_to_favorites": "Tambahkan ke favorit", @@ -3153,8 +3222,11 @@ "send.password_validation": "Kata sandi harus antara 8 dan 128 karakter.", "send.preview_button": "Pratinjau", "send.recipient_invalid": "Penerima tidak valid. Silakan periksa dan masukkan kembali", + "send.recipient_token_not_activated": "Akun penerima belum mengaktifkan token ini. Mohon minta penerima untuk menambahkan Token", "send.send_to_this_address": "Kirim ke alamat ini", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "Mengirim {0} memerlukan saldo akun minimal {1} {2}.", + "send.stellar_activation_minimum_hint": "Diperlukan minimal 1 XLM untuk mengaktifkan akun.", + "send.stellar_recipient_account_not_activated": "Akun penerima tidak diaktifkan di jaringan Stellar. Kirimkan setidaknya 1 XLM ke alamat ini untuk menginisialisasinya.", "send.str_minimum_balance_is_str": "Pengiriman gagal. Saldo minimum untuk {token} adalah {amount}.", "send.str_minimum_transfer": "Jumlah transfer minimum adalah {0}.", "send.suggest_reserving_str_as_gas_fee": "Sarankan untuk menyisihkan {0} sebagai biaya jaringan.", diff --git a/packages/shared/src/locale/json/it_IT.json b/packages/shared/src/locale/json/it_IT.json index 1e46d29fb7e5..49740d85bf63 100644 --- a/packages/shared/src/locale/json/it_IT.json +++ b/packages/shared/src/locale/json/it_IT.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "Compilato", "Limit.order_status_open": "Apri", "Limit.order_status_unfilled": "Non eseguito", + "Perps.BBO_Counterparty": "Controparte 1", + "Perps.BBO_Queue": "Coda 1", + "Perps.BBO_button_desc": "L'ordine Best-bid-offer (BBO) inserisce un ordine limite al miglior prezzo bid o ask. Se utilizzi Post-Only, il tuo ordine verrà inserito solo se aggiunge liquidità. A causa delle fluttuazioni di mercato, l'ordine potrebbe non essere inserito se il mercato si muove prima della conferma.", + "Perps.BBO_button_title": "BBO", + "Perps.BBO_select_title": "Seleziona modalità BBO", + "Perps.BBO_unavailable": "BBO non disponibile", "Perps.referral_bonus_from": "Bonus referral dal trading su Perps", "account_model.watched": "Visto", "account_name_form_helper_text": "Non inserire informazioni sensibili.", @@ -452,9 +458,66 @@ "date.today": "Oggi", "date.yesterday": "Ieri", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "Asset / Preso in Prestito", + "defi.asset_can_be_collateral": "Asset / Può Essere Collaterale", + "defi.asset_supplied": "Asset / Fornito", + "defi.assets_to_borrow": "Asset da Prendere in Prestito", + "defi.assets_to_supply": "Asset da Fornire", + "defi.available_liquidity": "Liquidità disponibile", + "defi.available_to_borrow": "Disponibile per il prestito", + "defi.borrow_apy": "APY di prestito", + "defi.borrow_cap_usage": "Utilizzo del limite di prestito", + "defi.borrowable": "Mutuabile", + "defi.borrowable_today": "Mutuabile oggi", + "defi.borrowed": "Preso in Prestito", + "defi.borrowed_balance": "Saldo preso a prestito", + "defi.can_be_collateral": "Può essere garanzia", + "defi.claim_all": "Richiedi Tutto", + "defi.claimable_rewards": "Premi Reclamabili", + "defi.current_utilization": "Utilizzo attuale", + "defi.daily_borrow_cap": "Limite di prestito giornaliero", + "defi.daily_cap_resets_in": "Il limite giornaliero si azzera tra", + "defi.daily_caps": "Limiti giornalieri", + "defi.daily_withdraw_cap": "Limite di prelievo giornaliero", + "defi.from_wallet_balance": "Dal Saldo del Portafoglio", + "defi.health_factor": "Fattore di Salute", + "defi.interest_rate_model": "Modello tasso di interesse", "defi.liquidation_acknowledge": "Riconosco i rischi coinvolti", + "defi.liquidation_at_less_than_1_00": "Liquidazione a < 1.00", "defi.liquidation_borrow_desc": "Richiedere un prestito di questa somma ridurrà il tuo fattore salute e aumenterà il rischio di liquidazione.", + "defi.liquidation_ltv": "LTV di liquidazione", "defi.liquidation_withdraw_desc": "Prelevare questa somma ridurrà il tuo fattore salute e aumenterà il rischio di liquidazione.", + "defi.manage_position": "Gestisci Posizione", + "defi.max_ltv": "LTV massimo", + "defi.my_borrow": "Il Mio Prestito", + "defi.my_info": "Le mie info", + "defi.my_supply": "La Mia Offerta", + "defi.net_apy": "APY Netto", + "defi.net_worth": "Patrimonio Netto", + "defi.no_assets_to_borrow": "Nessun bene disponibile per il prestito", + "defi.no_assets_to_supply": "Nessuna risorsa disponibile per la fornitura", + "defi.nothing_supplied_yet": "Niente ancora fornito", + "defi.oracle_price": "Prezzo oracolo", + "defi.platform_bonus": "Bonus Piattaforma", + "defi.refundable_fee": "Commissione Rimborsabile", + "defi.repay": "Rimborsare", + "defi.reserve_size": "Dimensione riserva", + "defi.safe_max": "Massimo Sicuro", + "defi.select_an_asset_to_borrow": "Seleziona un bene da prendere in prestito", + "defi.select_an_asset_to_supply": "Seleziona un asset da fornire", + "defi.show_assets_with_0_balance": "Mostra Asset con Saldo 0", + "defi.soft_liquidations": "Liquidazioni morbide", + "defi.supplied": "Fornito", + "defi.supplied_balance": "Saldo fornito", + "defi.supply": "Offerta", + "defi.supply_apy": "APY di fornitura", + "defi.supply_assets_as_collateral_before_borrowing": "Fornire asset come garanzia prima di prendere in prestito", + "defi.supply_cap_usage": "Utilizzo del limite di fornitura", + "defi.use_as_collateral": "Usa como Garanzia", + "defi.utilization_ratio": "Tasso di utilizzo", + "defi.view_reserve_details": "Visualizza Dettagli Riserva", + "defi.with_collateral": "Con Garanzia", + "defi.withdrawable_today": "Prelevabile oggi", "derivation_path": "Percorso di derivazione", "description_403": "I nostri servizi non sono disponibili nella tua regione.", "device.btc_only_coming_soon": "Questa funzionalità sarà disponibile in una versione successiva del firmware BTC-Only.", @@ -1162,6 +1225,7 @@ "global.approvals": "Approvazioni", "global.approve": "Approva", "global.apr": "APR", + "global.apy": "APY", "global.asset": "Asset", "global.at_least_variable_characters": "Almeno {variable} caratteri", "global.auto": "Auto", @@ -1182,6 +1246,7 @@ "global.bluetooth": "Bluetooth", "global.bluetooth_firmware": "Firmware Bluetooth", "global.bootloader": "Bootloader", + "global.borrow": "Prendi in prestito", "global.brightness": "Luminosità", "global.browser": "Browser", "global.bulk": "In blocco", @@ -1811,6 +1876,7 @@ "global.wallet": "Portafoglio", "global.wallet_activity": "Attività del portafoglio", "global.wallet_avatar": "Avatar del portafoglio", + "global.wallet_balance": "Saldo del portafoglio", "global.wallet_history_notification_banner": "Abilita le notifiche per ricevere aggiornamenti istantanei sull'attività del tuo portafoglio.", "global.wallets": "Portafogli", "global.wallpaper": "Sfondo", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "Autenticazione fallita, controlla la tua connessione di rete e prova di nuovo", "ln.payment_received_label": "Pagamento ricevuto", "log_out_confirmation_text": "Sei sicuro di voler uscire da {email}?", + "log_out_wallet": "Disconnetti wallet", + "log_out_wallet_desc": "Questo rimuoverà il portafoglio da questo dispositivo. Puoi accedere nuovamente in qualsiasi momento utilizzando il tuo account social e il PIN.", "logged_out_feedback": "Disconnessione riuscita", "login.forgot_passcode": "Hai dimenticato il codice?", "login.forgot_password": "Hai dimenticato la password?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "Transazioni 24 Ore", "market.24h_vol_usd": "VOL 24H(USD)", "market.30d": "30G", + "market.3m": "3M", "market.7d": "7G", "market.add_number_tokens": "Aggiungi {number} token", "market.add_to_favorites": "Aggiungi ai preferiti", @@ -3153,8 +3222,11 @@ "send.password_validation": "La password deve essere compresa tra 8 e 128 caratteri.", "send.preview_button": "Anteprima", "send.recipient_invalid": "Destinatario non valido. Si prega di controllare e reinserire", + "send.recipient_token_not_activated": "L'account del destinatario non ha attivato questo token. Chiedere al destinatario di aggiungere un token", "send.send_to_this_address": "Invia a questo indirizzo", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "Per inviare {0} è necessario un saldo contabile di almeno {1} {2}.", + "send.stellar_activation_minimum_hint": "Per attivare il conto è necessario un minimo di 1 XLM.", + "send.stellar_recipient_account_not_activated": "Il conto del destinatario non è attivato sulla rete Stellar. Si prega di inviare almeno 1 XLM a questo indirizzo per inizializzarlo.", "send.str_minimum_balance_is_str": "Invio non riuscito. Il saldo minimo per {token} è di {amount}.", "send.str_minimum_transfer": "L'importo minimo del trasferimento è {0}.", "send.suggest_reserving_str_as_gas_fee": "Suggeriamo di riservare {0} come tariffa di rete.", diff --git a/packages/shared/src/locale/json/ja_JP.json b/packages/shared/src/locale/json/ja_JP.json index e042970410dc..24ab632271fa 100644 --- a/packages/shared/src/locale/json/ja_JP.json +++ b/packages/shared/src/locale/json/ja_JP.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "埋められた", "Limit.order_status_open": "オープン", "Limit.order_status_unfilled": "未約定", + "Perps.BBO_Counterparty": "取引相手 1", + "Perps.BBO_Queue": "キュー 1", + "Perps.BBO_button_desc": "ベスト・ビッド・オファー(BBO)は、最良の買い気配または売り気配の価格で指値注文を発注します。Post-Onlyを使用する場合、注文は流動性を追加する場合にのみ発注されます。市場の変動により、確定前に市場が動いた場合、注文の発注に失敗する可能性があります。", + "Perps.BBO_button_title": "BBO", + "Perps.BBO_select_title": "BBOモードを選択", + "Perps.BBO_unavailable": "BBO利用不可", "Perps.referral_bonus_from": "Perpsでの取引による紹介ボーナス", "account_model.watched": "視聴済み", "account_name_form_helper_text": "機密情報を入力しないでください。", @@ -452,9 +458,66 @@ "date.today": "今日", "date.yesterday": "昨日", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "資産 / 借り入れた", + "defi.asset_can_be_collateral": "資産 / 担保にできる", + "defi.asset_supplied": "資産 / 供給済み", + "defi.assets_to_borrow": "借りる資産", + "defi.assets_to_supply": "供給する資産", + "defi.available_liquidity": "利用可能な流動性", + "defi.available_to_borrow": "借り入れ可能", + "defi.borrow_apy": "借入 APY", + "defi.borrow_cap_usage": "借入上限使用", + "defi.borrowable": "借り入れ可能", + "defi.borrowable_today": "本日借り入れ可能", + "defi.borrowed": "借り入れた", + "defi.borrowed_balance": "借入残高", + "defi.can_be_collateral": "担保可能", + "defi.claim_all": "全て請求", + "defi.claimable_rewards": "受け取り可能な報酬", + "defi.current_utilization": "現在の利用率", + "defi.daily_borrow_cap": "日次借入上限", + "defi.daily_cap_resets_in": "日次上限のリセットまで", + "defi.daily_caps": "日次上限", + "defi.daily_withdraw_cap": "日次出金上限", + "defi.from_wallet_balance": "ウォレット残高から", + "defi.health_factor": "健全性係数", + "defi.interest_rate_model": "金利モデル", "defi.liquidation_acknowledge": "私はそれに伴うリスクを認識しています", + "defi.liquidation_at_less_than_1_00": "< 1.00 での清算", "defi.liquidation_borrow_desc": "この金額を借り入れると、健全性要因が低下し、清算のリスクが高まります。", + "defi.liquidation_ltv": "清算LTV", "defi.liquidation_withdraw_desc": "この金額を引き出すと、健全性要因が低下し、清算のリスクが高まります。", + "defi.manage_position": "ポジションを管理", + "defi.max_ltv": "最大LTV", + "defi.my_borrow": "私の借り入れ", + "defi.my_info": "私の情報", + "defi.my_supply": "私の供給", + "defi.net_apy": "純 APY", + "defi.net_worth": "純資産", + "defi.no_assets_to_borrow": "借りられる資産がない", + "defi.no_assets_to_supply": "供給可能な資産がありません", + "defi.nothing_supplied_yet": "まだ何も供給されていません", + "defi.oracle_price": "オラクル価格", + "defi.platform_bonus": "プラットフォームボーナス", + "defi.refundable_fee": "返金可能な手数料", + "defi.repay": "返済", + "defi.reserve_size": "準備規模", + "defi.safe_max": "安全な最大値", + "defi.select_an_asset_to_borrow": "借りる資産を選択する", + "defi.select_an_asset_to_supply": "供給する資産を選択してください", + "defi.show_assets_with_0_balance": "残高 0 の資産を表示", + "defi.soft_liquidations": "ソフト清算", + "defi.supplied": "供給済み", + "defi.supplied_balance": "供給残高", + "defi.supply": "供給", + "defi.supply_apy": "供給 APY", + "defi.supply_assets_as_collateral_before_borrowing": "借りる前に資産を担保として供給してください", + "defi.supply_cap_usage": "供給上限使用", + "defi.use_as_collateral": "担保として使用する", + "defi.utilization_ratio": "利用率", + "defi.view_reserve_details": "準備金詳細を表示", + "defi.with_collateral": "担保付き", + "defi.withdrawable_today": "本日出金可能", "derivation_path": "導出パス", "description_403": "弊社のサービスはお客様の地域ではご利用いただけません。", "device.btc_only_coming_soon": "この機能は、今後のBTC専用ファームウェアリリースで利用可能になります。", @@ -1162,6 +1225,7 @@ "global.approvals": "承認", "global.approve": "承認", "global.apr": "APR", + "global.apy": "APY", "global.asset": "資産", "global.at_least_variable_characters": "少なくとも{variable}文字", "global.auto": "自動", @@ -1182,6 +1246,7 @@ "global.bluetooth": "Bluetooth", "global.bluetooth_firmware": "Bluetoothファームウェア", "global.bootloader": "Bootloader", + "global.borrow": "借りる", "global.brightness": "明るさ", "global.browser": "ブラウザ", "global.bulk": "一括", @@ -1811,6 +1876,7 @@ "global.wallet": "ウォレット", "global.wallet_activity": "ウォレットアクティビティ", "global.wallet_avatar": "ウォレットアバター", + "global.wallet_balance": "ウォレット残高", "global.wallet_history_notification_banner": "通知を有効にして、ウォレットのアクティビティに関する最新情報を即座に受け取りましょう。", "global.wallets": "ウォレット", "global.wallpaper": "壁紙", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "認証に失敗しました、ネットワーク接続を確認して再試行してください", "ln.payment_received_label": "お支払い頂いた", "log_out_confirmation_text": "{email} からログアウトしてもよろしいですか?", + "log_out_wallet": "ウォレットからログアウト", + "log_out_wallet_desc": "これにより、このデバイスからウォレットが削除されます。ソーシャルアカウントとPINを使用していつでも再度ログインできます。", "logged_out_feedback": "ログアウトしました", "login.forgot_passcode": "パスコードを忘れましたか?", "login.forgot_password": "パスワードを忘れましたか?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "24時間取引", "market.24h_vol_usd": "24時間ボリューム (USD)", "market.30d": "30日", + "market.3m": "3か月", "market.7d": "7日", "market.add_number_tokens": "{number}トークンを追加", "market.add_to_favorites": "お気に入りに追加", @@ -3153,8 +3222,11 @@ "send.password_validation": "パスワードは8文字から128文字の間でなければなりません。", "send.preview_button": "プレビュー", "send.recipient_invalid": "無効な受信者です。確認して再入力してください", + "send.recipient_token_not_activated": "受信者のアカウントがこのトークンを有効化していません。受信者にトークンの追加を依頼してください。", "send.send_to_this_address": "このアドレスに送信", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "{0}を送信するには、少なくとも{1} {2}のアカウント残高が必要です。", + "send.stellar_activation_minimum_hint": "アカウントの有効化には最低1XLMが必要です。", + "send.stellar_recipient_account_not_activated": "受信者アカウントはステラネットワーク上で有効化されていません。このアドレスに最低1XLMを送金して初期化してください。", "send.str_minimum_balance_is_str": "送信に失敗しました。{token}の最低残高は{amount}です。", "send.str_minimum_transfer": "最小転送量は{0}です。", "send.suggest_reserving_str_as_gas_fee": "{0}をネットワーク料として予約することを提案します。", diff --git a/packages/shared/src/locale/json/ko_KR.json b/packages/shared/src/locale/json/ko_KR.json index a93a54fa37ef..5025b4be2285 100644 --- a/packages/shared/src/locale/json/ko_KR.json +++ b/packages/shared/src/locale/json/ko_KR.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "채워짐", "Limit.order_status_open": "열기", "Limit.order_status_unfilled": "미체결", + "Perps.BBO_Counterparty": "거래상대방 1", + "Perps.BBO_Queue": "대기열 1", + "Perps.BBO_button_desc": "베스트 비드 오퍼(BBO)는 최우선 매수 호가 혹은 매도 호가에 지정가 주문을 제출하는 방식입니다. Post-Only를 사용하는 경우, 주문은 유동성을 추가할 때에만 제출됩니다. 시장 변동으로 인해, 체결이 확정되기 전에 시세가 변동하면 주문 제출이 실패할 수 있습니다.", + "Perps.BBO_button_title": "BBO", + "Perps.BBO_select_title": "최우선호가 모드 선택", + "Perps.BBO_unavailable": "최우가격 이용 불가", "Perps.referral_bonus_from": "퍼프스 거래 추천 보너스", "account_model.watched": "시청함", "account_name_form_helper_text": "민감한 정보를 입력하지 마세요.", @@ -452,9 +458,66 @@ "date.today": "오늘", "date.yesterday": "어제", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "자산 / 빌린", + "defi.asset_can_be_collateral": "자산 / 담보 가능", + "defi.asset_supplied": "자산 / 공급됨", + "defi.assets_to_borrow": "빌릴 자산", + "defi.assets_to_supply": "공급할 자산", + "defi.available_liquidity": "사용 가능한 유동성", + "defi.available_to_borrow": "대출 가능", + "defi.borrow_apy": "차입 APY", + "defi.borrow_cap_usage": "차입 한도 사용", + "defi.borrowable": "대출 가능", + "defi.borrowable_today": "오늘 대출 가능", + "defi.borrowed": "빌린", + "defi.borrowed_balance": "대출 잔액", + "defi.can_be_collateral": "담보 가능", + "defi.claim_all": "모두 청구", + "defi.claimable_rewards": "청구 가능 보상", + "defi.current_utilization": "현재 활용률", + "defi.daily_borrow_cap": "일일 차입 한도", + "defi.daily_cap_resets_in": "일일 한도 재설정 시간", + "defi.daily_caps": "일일 한도", + "defi.daily_withdraw_cap": "일일 출금 한도", + "defi.from_wallet_balance": "지갑 잔액에서", + "defi.health_factor": "건강 계수", + "defi.interest_rate_model": "이자율 모델", "defi.liquidation_acknowledge": "저는 관련된 위험을 인지하고 있습니다.", + "defi.liquidation_at_less_than_1_00": "< 1.00 에서 청산", "defi.liquidation_borrow_desc": "이 금액을 빌리면 건강 상태가 악화되고 파산 위험이 높아집니다.", + "defi.liquidation_ltv": "청산 LTV", "defi.liquidation_withdraw_desc": "이 금액을 인출하면 건강 상태가 악화되고 파산 위험이 높아집니다.", + "defi.manage_position": "포지션 관리", + "defi.max_ltv": "최대 LTV", + "defi.my_borrow": "내 빌리기", + "defi.my_info": "내 정보", + "defi.my_supply": "내 공급", + "defi.net_apy": "순 APY", + "defi.net_worth": "순자산", + "defi.no_assets_to_borrow": "대출 가능한 자산이 없습니다.", + "defi.no_assets_to_supply": "공급 가능한 자산이 없습니다.", + "defi.nothing_supplied_yet": "아직 공급된 것이 없습니다", + "defi.oracle_price": "오라클 가격", + "defi.platform_bonus": "플랫폼 보너스", + "defi.refundable_fee": "환불 가능한 수수료", + "defi.repay": "갚다", + "defi.reserve_size": "준비금 규모", + "defi.safe_max": "안전 최대", + "defi.select_an_asset_to_borrow": "빌릴 자산을 선택하세요", + "defi.select_an_asset_to_supply": "공급할 자산을 선택하십시오", + "defi.show_assets_with_0_balance": "잔고 0 인 자산 표시", + "defi.soft_liquidations": "소프트 청산", + "defi.supplied": "공급됨", + "defi.supplied_balance": "공급 잔액", + "defi.supply": "공급", + "defi.supply_apy": "공급 APY", + "defi.supply_assets_as_collateral_before_borrowing": "빌리기 전에 자산을 담보로 공급하십시오", + "defi.supply_cap_usage": "공급 상한 사용", + "defi.use_as_collateral": "담보로 사용", + "defi.utilization_ratio": "활용률", + "defi.view_reserve_details": "예비 세부 정보 보기", + "defi.with_collateral": "담보와 함께", + "defi.withdrawable_today": "오늘 출금 가능", "derivation_path": "유도 경로", "description_403": "귀하의 지역에서는 당사 서비스를 이용할 수 없습니다.", "device.btc_only_coming_soon": "이 기능은 이후에 출시될 BTC 전용 펌웨어에서 제공될 예정입니다.", @@ -1162,6 +1225,7 @@ "global.approvals": "승인", "global.approve": "승인", "global.apr": "APR", + "global.apy": "APY", "global.asset": "자산", "global.at_least_variable_characters": "최소 {variable}자", "global.auto": "자동", @@ -1182,6 +1246,7 @@ "global.bluetooth": "블루투스", "global.bluetooth_firmware": "블루투스 펌웨어", "global.bootloader": "Bootloader", + "global.borrow": "빌리기", "global.brightness": "밝기", "global.browser": "브라우저", "global.bulk": "일괄", @@ -1811,6 +1876,7 @@ "global.wallet": "지갑", "global.wallet_activity": "지갑 활동", "global.wallet_avatar": "지갑 아바타", + "global.wallet_balance": "지갑 잔액", "global.wallet_history_notification_banner": "지갑 활동에 대한 최신 정보를 바로 받으려면 알림을 활성화하세요.", "global.wallets": "지갑", "global.wallpaper": "배경화면", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "인증에 실패했습니다, 네트워크 연결을 확인하고 다시 시도해보세요", "ln.payment_received_label": "지불 수신", "log_out_confirmation_text": "{email}에서 로그아웃하시겠습니까?", + "log_out_wallet": "지갑 로그아웃", + "log_out_wallet_desc": "이 작업을 하면 이 기기에서 지갑이 삭제됩니다. 이후에도 소셜 계정과 PIN을 사용해 언제든 다시 로그인할 수 있습니다.", "logged_out_feedback": "성공적으로 로그아웃되었습니다", "login.forgot_passcode": "암호를 잊으셨나요?", "login.forgot_password": "비밀번호를 잊으셨나요?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "24시간 거래", "market.24h_vol_usd": "24시간 거래량 (USD)", "market.30d": "30일", + "market.3m": "3개월", "market.7d": "7일", "market.add_number_tokens": "{number} 토큰 추가", "market.add_to_favorites": "즐겨찾기에 추가", @@ -3153,8 +3222,11 @@ "send.password_validation": "비밀번호는 8자에서 128자 사이여야 합니다.", "send.preview_button": "미리보기", "send.recipient_invalid": "잘못된 수신자입니다. 확인 후 다시 입력해 주세요", + "send.recipient_token_not_activated": "받는 사람 계정에서 이 토큰을 활성화하지 않았습니다. 받는 사람에게 토큰을 추가하도록 요청하세요", "send.send_to_this_address": "이 주소로 보내기", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "{0}을 보내려면 계좌 잔액이 최소한 {1} {2} 이상이어야 합니다.", + "send.stellar_activation_minimum_hint": "계정을 활성화하려면 최소 1XLM이 필요합니다.", + "send.stellar_recipient_account_not_activated": "수취인 계정이 스텔라 네트워크에서 활성화되지 않았습니다. 초기화하려면 이 주소로 최소 1XLM을 보내주세요.", "send.str_minimum_balance_is_str": "전송 실패. {token}의 최소 잔액은 {amount}입니다.", "send.str_minimum_transfer": "최소 이체 금액은 {0}입니다.", "send.suggest_reserving_str_as_gas_fee": "{0}을 네트워크 수수료로 예약하는 것을 제안합니다.", diff --git a/packages/shared/src/locale/json/pt.json b/packages/shared/src/locale/json/pt.json index ebe92a369e93..cec7debd2146 100644 --- a/packages/shared/src/locale/json/pt.json +++ b/packages/shared/src/locale/json/pt.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "Preenchido", "Limit.order_status_open": "Abrir", "Limit.order_status_unfilled": "Não preenchido", + "Perps.BBO_Counterparty": "Contraparte 1", + "Perps.BBO_Queue": "Fila 1", + "Perps.BBO_button_desc": "Melhor oferta de compra e venda (BBO) coloca uma ordem limitada ao melhor preço de compra ou venda. Se você usar Somente Publicação, sua ordem só será colocada se adicionar liquidez. Devido a flutuações do mercado, a ordem pode falhar ao ser colocada se o mercado se mover antes de ser confirmada.", + "Perps.BBO_button_title": "BBO", + "Perps.BBO_select_title": "Selecionar Modo BBO", + "Perps.BBO_unavailable": "BBO Indisponível", "Perps.referral_bonus_from": "Bônus de indicação por negociação em Perps", "account_model.watched": "Assistido", "account_name_form_helper_text": "Não insira informações confidenciais.", @@ -452,9 +458,66 @@ "date.today": "Hoje", "date.yesterday": "Ontem", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "Ativo / Emprestado", + "defi.asset_can_be_collateral": "Ativo / Pode ser Garantia", + "defi.asset_supplied": "Ativo / Fornecido", + "defi.assets_to_borrow": "Ativos para Emprestar", + "defi.assets_to_supply": "Ativos para Fornecer", + "defi.available_liquidity": "Liquidez disponível", + "defi.available_to_borrow": "Disponível para empréstimo", + "defi.borrow_apy": "APY de empréstimo", + "defi.borrow_cap_usage": "Uso do limite de empréstimo", + "defi.borrowable": "Emprestável", + "defi.borrowable_today": "Emprestável hoje", + "defi.borrowed": "Emprestado", + "defi.borrowed_balance": "Saldo emprestado", + "defi.can_be_collateral": "Pode ser garantia", + "defi.claim_all": "Reclamar Tudo", + "defi.claimable_rewards": "Recompensas Resgatáveis", + "defi.current_utilization": "Utilização atual", + "defi.daily_borrow_cap": "Limite diário de empréstimo", + "defi.daily_cap_resets_in": "Limite diário reinicia em", + "defi.daily_caps": "Limites diários", + "defi.daily_withdraw_cap": "Limite diário de saque", + "defi.from_wallet_balance": "Do Saldo da Carteira", + "defi.health_factor": "Fator de Saúde", + "defi.interest_rate_model": "Modelo de taxa de juros", "defi.liquidation_acknowledge": "Reconheço os riscos envolvidos.", + "defi.liquidation_at_less_than_1_00": "Liquidação em < 1.00", "defi.liquidation_borrow_desc": "Contrair esse empréstimo reduzirá seu fator de saúde e aumentará o risco de falência.", + "defi.liquidation_ltv": "LTV de liquidação", "defi.liquidation_withdraw_desc": "Retirar esse valor reduzirá seu fator de saúde e aumentará o risco de falência.", + "defi.manage_position": "Gerenciar Posição", + "defi.max_ltv": "LTV Máximo", + "defi.my_borrow": "Meu Empréstimo", + "defi.my_info": "Minhas informações", + "defi.my_supply": "Minha Oferta", + "defi.net_apy": "APY Líquido", + "defi.net_worth": "Patrimônio Líquido", + "defi.no_assets_to_borrow": "Não há ativos disponíveis para empréstimo.", + "defi.no_assets_to_supply": "Não há ativos disponíveis para fornecimento.", + "defi.nothing_supplied_yet": "Nada fornecido ainda", + "defi.oracle_price": "Preço do oráculo", + "defi.platform_bonus": "Bônus da Plataforma", + "defi.refundable_fee": "Taxa Reembolsável", + "defi.repay": "Pagar", + "defi.reserve_size": "Tamanho da reserva", + "defi.safe_max": "Máximo Seguro", + "defi.select_an_asset_to_borrow": "Selecione um ativo para empréstimo.", + "defi.select_an_asset_to_supply": "Selecione um ativo para fornecer", + "defi.show_assets_with_0_balance": "Mostrar Ativos com Saldo 0", + "defi.soft_liquidations": "Liquidações suaves", + "defi.supplied": "Fornecido", + "defi.supplied_balance": "Saldo fornecido", + "defi.supply": "Fornecer", + "defi.supply_apy": "APY de fornecimento", + "defi.supply_assets_as_collateral_before_borrowing": "Fornecer ativos como garantia antes de emprestar", + "defi.supply_cap_usage": "Uso do limite de fornecimento", + "defi.use_as_collateral": "Usar como Garantia", + "defi.utilization_ratio": "Taxa de utilização", + "defi.view_reserve_details": "Ver Detalhes da Reserva", + "defi.with_collateral": "Com Garantia", + "defi.withdrawable_today": "Retirável hoje", "derivation_path": "Caminho de derivação", "description_403": "Nossos serviços não estão disponíveis em sua região.", "device.btc_only_coming_soon": "Este recurso estará disponível em uma versão posterior do firmware BTC-Only.", @@ -1162,6 +1225,7 @@ "global.approvals": "Aprovações", "global.approve": "Aprovar", "global.apr": "APR", + "global.apy": "APY", "global.asset": "Ativo", "global.at_least_variable_characters": "Pelo menos {variable} caracteres", "global.auto": "Auto", @@ -1182,6 +1246,7 @@ "global.bluetooth": "Bluetooth", "global.bluetooth_firmware": "Firmware Bluetooth", "global.bootloader": "Bootloader", + "global.borrow": "Emprestar", "global.brightness": "Brilho", "global.browser": "Navegador", "global.bulk": "Em massa", @@ -1811,6 +1876,7 @@ "global.wallet": "Carteira", "global.wallet_activity": "Atividade da carteira", "global.wallet_avatar": "Avatar da carteira", + "global.wallet_balance": "Saldo da carteira", "global.wallet_history_notification_banner": "Ative as notificações para receber atualizações instantâneas sobre a atividade da sua carteira.", "global.wallets": "Carteiras", "global.wallpaper": "Papel de parede", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "A autenticação falhou, verifique sua conexão de rede e tente novamente", "ln.payment_received_label": "Pagamento recebido", "log_out_confirmation_text": "Tem certeza de que deseja sair de {email}?", + "log_out_wallet": "Sair da carteira", + "log_out_wallet_desc": "Isso removerá a carteira deste dispositivo. Você pode fazer login novamente a qualquer momento usando sua conta social e PIN.", "logged_out_feedback": "Sessão encerrada com sucesso", "login.forgot_passcode": "Esqueceu o código de acesso?", "login.forgot_password": "Esqueceu a senha?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "Transações 24H", "market.24h_vol_usd": "VOL 24H(USD)", "market.30d": "30D", + "market.3m": "3M", "market.7d": "7D", "market.add_number_tokens": "Adicione {number} tokens", "market.add_to_favorites": "Adicionar aos favoritos", @@ -3153,8 +3222,11 @@ "send.password_validation": "A senha deve ter entre 8 e 128 caracteres.", "send.preview_button": "Pré-visualização", "send.recipient_invalid": "Destinatário inválido. Por favor, verifique e digite novamente", + "send.recipient_token_not_activated": "A conta do destinatário não activou este token. Peça ao destinatário para adicionar um token", "send.send_to_this_address": "Enviar para este endereço", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "Enviar {0} requer um saldo em conta de pelo menos {1} {2}.", + "send.stellar_activation_minimum_hint": "É necessário um mínimo de 1 XLM para ativar a conta.", + "send.stellar_recipient_account_not_activated": "A conta do destinatário não está activada na rede Stellar. Por favor, envie pelo menos 1 XLM para este endereço para a inicializar.", "send.str_minimum_balance_is_str": "O envio falhou. O saldo mínimo para {token} é de {amount}.", "send.str_minimum_transfer": "O valor mínimo para transferência é {0}.", "send.suggest_reserving_str_as_gas_fee": "Sugere reservar {0} como taxa de rede.", diff --git a/packages/shared/src/locale/json/pt_BR.json b/packages/shared/src/locale/json/pt_BR.json index aae0ce39355f..bf671f35124d 100644 --- a/packages/shared/src/locale/json/pt_BR.json +++ b/packages/shared/src/locale/json/pt_BR.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "Preenchido", "Limit.order_status_open": "Abrir", "Limit.order_status_unfilled": "Não preenchido", + "Perps.BBO_Counterparty": "Contraparte 1", + "Perps.BBO_Queue": "Fila 1", + "Perps.BBO_button_desc": "A melhor oferta de compra e venda (BBO) coloca uma ordem limitada no melhor preço de compra ou venda. Se você usar Somente Publicação, sua ordem só será colocada se adicionar liquidez. Devido a flutuações do mercado, a ordem pode falhar ao ser colocada se o mercado se mover antes de ser confirmada.", + "Perps.BBO_button_title": "BBO", + "Perps.BBO_select_title": "Selecionar Modo BBO", + "Perps.BBO_unavailable": "BBO Indisponível", "Perps.referral_bonus_from": "Bônus de indicação por negociação em Perps", "account_model.watched": "Assistido", "account_name_form_helper_text": "Não insira informações confidenciais.", @@ -452,9 +458,66 @@ "date.today": "Hoje", "date.yesterday": "Ontem", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "Ativo / Emprestado", + "defi.asset_can_be_collateral": "Ativo / Pode ser Garantia", + "defi.asset_supplied": "Ativo / Fornecido", + "defi.assets_to_borrow": "Ativos para Emprestar", + "defi.assets_to_supply": "Ativos para Fornecer", + "defi.available_liquidity": "Liquidez disponível", + "defi.available_to_borrow": "Disponível para empréstimo", + "defi.borrow_apy": "APY de empréstimo", + "defi.borrow_cap_usage": "Uso do limite de empréstimo", + "defi.borrowable": "Emprestável", + "defi.borrowable_today": "Emprestável hoje", + "defi.borrowed": "Emprestado", + "defi.borrowed_balance": "Saldo emprestado", + "defi.can_be_collateral": "Pode ser garantia", + "defi.claim_all": "Reclamar Tudo", + "defi.claimable_rewards": "Recompensas Resgatáveis", + "defi.current_utilization": "Utilização atual", + "defi.daily_borrow_cap": "Limite diário de empréstimo", + "defi.daily_cap_resets_in": "Limite diário reinicia em", + "defi.daily_caps": "Limites diários", + "defi.daily_withdraw_cap": "Limite diário de saque", + "defi.from_wallet_balance": "Do Saldo da Carteira", + "defi.health_factor": "Fator de Saúde", + "defi.interest_rate_model": "Modelo de taxa de juros", "defi.liquidation_acknowledge": "Reconheço os riscos envolvidos.", + "defi.liquidation_at_less_than_1_00": "Liquidação em < 1.00", "defi.liquidation_borrow_desc": "Contrair esse empréstimo reduzirá seu fator de saúde e aumentará o risco de falência.", + "defi.liquidation_ltv": "LTV de liquidação", "defi.liquidation_withdraw_desc": "Retirar esse valor reduzirá seu fator de saúde e aumentará o risco de falência.", + "defi.manage_position": "Gerenciar Posição", + "defi.max_ltv": "LTV Máximo", + "defi.my_borrow": "Meu Empréstimo", + "defi.my_info": "Minhas informações", + "defi.my_supply": "Minha Oferta", + "defi.net_apy": "APY Líquido", + "defi.net_worth": "Patrimônio Líquido", + "defi.no_assets_to_borrow": "Não há ativos disponíveis para empréstimo.", + "defi.no_assets_to_supply": "Não há ativos disponíveis para fornecimento.", + "defi.nothing_supplied_yet": "Nada fornecido ainda", + "defi.oracle_price": "Preço do oráculo", + "defi.platform_bonus": "Bônus da Plataforma", + "defi.refundable_fee": "Taxa Reembolsável", + "defi.repay": "Pagar", + "defi.reserve_size": "Tamanho da reserva", + "defi.safe_max": "Máximo Seguro", + "defi.select_an_asset_to_borrow": "Selecione um ativo para empréstimo.", + "defi.select_an_asset_to_supply": "Selecione um ativo para fornecer", + "defi.show_assets_with_0_balance": "Mostrar Ativos com Saldo 0", + "defi.soft_liquidations": "Liquidações suaves", + "defi.supplied": "Fornecido", + "defi.supplied_balance": "Saldo fornecido", + "defi.supply": "Fornecer", + "defi.supply_apy": "APY de fornecimento", + "defi.supply_assets_as_collateral_before_borrowing": "Fornecer ativos como garantia antes de emprestar", + "defi.supply_cap_usage": "Uso do limite de fornecimento", + "defi.use_as_collateral": "Usar como Garantia", + "defi.utilization_ratio": "Taxa de utilização", + "defi.view_reserve_details": "Ver Detalhes da Reserva", + "defi.with_collateral": "Com Garantia", + "defi.withdrawable_today": "Retirável hoje", "derivation_path": "Caminho de derivação", "description_403": "Nossos serviços não estão disponíveis em sua região.", "device.btc_only_coming_soon": "Este recurso estará disponível em uma versão futura do firmware BTC-Only.", @@ -1162,6 +1225,7 @@ "global.approvals": "Aprovações", "global.approve": "Aprovar", "global.apr": "APR", + "global.apy": "APY", "global.asset": "Ativo", "global.at_least_variable_characters": "Pelo menos {variable} caracteres", "global.auto": "Automático", @@ -1182,6 +1246,7 @@ "global.bluetooth": "Bluetooth", "global.bluetooth_firmware": "Firmware Bluetooth", "global.bootloader": "Bootloader", + "global.borrow": "Emprestar", "global.brightness": "Brilho", "global.browser": "Navegador", "global.bulk": "Em massa", @@ -1811,6 +1876,7 @@ "global.wallet": "Carteira", "global.wallet_activity": "Atividade da carteira", "global.wallet_avatar": "Avatar da carteira", + "global.wallet_balance": "Saldo da carteira", "global.wallet_history_notification_banner": "Ative as notificações para receber atualizações instantâneas sobre a atividade da sua carteira.", "global.wallets": "Carteiras", "global.wallpaper": "Papel de parede", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "A autenticação falhou, verifique sua conexão de rede e tente novamente", "ln.payment_received_label": "Pagamento recebido", "log_out_confirmation_text": "Tem certeza de que deseja sair de {email}?", + "log_out_wallet": "Desconectar carteira", + "log_out_wallet_desc": "Isso removerá a carteira deste dispositivo. Você pode fazer login novamente a qualquer momento usando sua conta social e PIN.", "logged_out_feedback": "Logout realizado com sucesso", "login.forgot_passcode": "Esqueceu a senha?", "login.forgot_password": "Esqueceu a senha?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "Transações 24H", "market.24h_vol_usd": "VOL 24H(USD)", "market.30d": "30D", + "market.3m": "3M", "market.7d": "7D", "market.add_number_tokens": "Adicione {number} tokens", "market.add_to_favorites": "Adicionar aos favoritos", @@ -3153,8 +3222,11 @@ "send.password_validation": "A senha deve ter entre 8 e 128 caracteres.", "send.preview_button": "Pré-visualização", "send.recipient_invalid": "Destinatário inválido. Por favor, verifique e digite novamente", + "send.recipient_token_not_activated": "A conta do destinatário não ativou esse token. Solicite ao destinatário que adicione um token", "send.send_to_this_address": "Enviar para este endereço", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "Enviar {0} requer um saldo em conta de pelo menos {1} {2}.", + "send.stellar_activation_minimum_hint": "É necessário um mínimo de 1 XLM para ativar a conta.", + "send.stellar_recipient_account_not_activated": "A conta do destinatário não está ativada na rede Stellar. Por favor, envie pelo menos 1 XLM para este endereço para inicializá-la.", "send.str_minimum_balance_is_str": "O envio falhou. O saldo mínimo para {token} é de {amount}.", "send.str_minimum_transfer": "O valor mínimo para transferência é {0}.", "send.suggest_reserving_str_as_gas_fee": "Sugere reservar {0} como taxa de rede.", diff --git a/packages/shared/src/locale/json/ru.json b/packages/shared/src/locale/json/ru.json index c4fc66acb3e2..0a5698f3d146 100644 --- a/packages/shared/src/locale/json/ru.json +++ b/packages/shared/src/locale/json/ru.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "Заполнено", "Limit.order_status_open": "Открыть", "Limit.order_status_unfilled": "Неисполненный", + "Perps.BBO_Counterparty": "Контрагент 1", + "Perps.BBO_Queue": "Очередь 1", + "Perps.BBO_button_desc": "Лучшая цена покупки-продажи (BBO) размещает лимитный ордер по лучшей цене покупки или продажи. Если вы используете режим Post-Only, ваш ордер будет размещен только в том случае, если он добавляет ликвидность. Из-за колебаний рынка ордер может не быть размещен, если рынок изменится до его подтверждения.", + "Perps.BBO_button_title": "BBO", + "Perps.BBO_select_title": "Выберите режим BBO", + "Perps.BBO_unavailable": "Лучшая цена недоступна", "Perps.referral_bonus_from": "Реферальный бонус от торговли на Perps", "account_model.watched": "Просмотрено", "account_name_form_helper_text": "Не вводите конфиденциальную информацию.", @@ -452,9 +458,66 @@ "date.today": "Сегодня", "date.yesterday": "Вчера", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "Актив / Заимствовано", + "defi.asset_can_be_collateral": "Актив / Может быть залогом", + "defi.asset_supplied": "Актив / Поставлено", + "defi.assets_to_borrow": "Активы для заимствования", + "defi.assets_to_supply": "Активы для поставки", + "defi.available_liquidity": "Доступная ликвидность", + "defi.available_to_borrow": "Доступно для заимствования", + "defi.borrow_apy": "APY заимствования", + "defi.borrow_cap_usage": "Использование лимита заимствования", + "defi.borrowable": "Доступно для заимствования", + "defi.borrowable_today": "Доступно для заимствования сегодня", + "defi.borrowed": "Заимствовано", + "defi.borrowed_balance": "Заимствованный баланс", + "defi.can_be_collateral": "Может быть обеспечением", + "defi.claim_all": "Забрать все", + "defi.claimable_rewards": "Доступные награды", + "defi.current_utilization": "Текущая утилизация", + "defi.daily_borrow_cap": "Дневной лимит заимствования", + "defi.daily_cap_resets_in": "Дневной лимит сбросится через", + "defi.daily_caps": "Дневные лимиты", + "defi.daily_withdraw_cap": "Дневной лимит вывода", + "defi.from_wallet_balance": "Из баланса кошелька", + "defi.health_factor": "Фактор здоровья", + "defi.interest_rate_model": "Модель процентной ставки", "defi.liquidation_acknowledge": "Я осознаю связанные с этим риски.", + "defi.liquidation_at_less_than_1_00": "Ликвидация при < 1.00", "defi.liquidation_borrow_desc": "Заимствование такой суммы снизит ваш коэффициент здоровья и увеличит риск невозврата кредита.", + "defi.liquidation_ltv": "Ликвидационный LTV", "defi.liquidation_withdraw_desc": "Снятие этой суммы ухудшит ваше состояние здоровья и увеличит риск потери средств.", + "defi.manage_position": "Управление позицией", + "defi.max_ltv": "Максимальный LTV", + "defi.my_borrow": "Мой заем", + "defi.my_info": "Моя информация", + "defi.my_supply": "Мой запас", + "defi.net_apy": "Чистый APY", + "defi.net_worth": "Чистая стоимость", + "defi.no_assets_to_borrow": "Нет активов, доступных для заимствования.", + "defi.no_assets_to_supply": "Ресурсов для поставки нет", + "defi.nothing_supplied_yet": "Пока ничего не поставлено", + "defi.oracle_price": "Цена оракула", + "defi.platform_bonus": "Бонус платформы", + "defi.refundable_fee": "Возвращаемый сбор", + "defi.repay": "Погасить", + "defi.reserve_size": "Размер резерва", + "defi.safe_max": "Безопасный максимум", + "defi.select_an_asset_to_borrow": "Выберите актив для заимствования", + "defi.select_an_asset_to_supply": "Выберите актив для поставки", + "defi.show_assets_with_0_balance": "Показать активы с балансом 0", + "defi.soft_liquidations": "Мягкие ликвидации", + "defi.supplied": "Поставлено", + "defi.supplied_balance": "Предоставленный баланс", + "defi.supply": "Поставка", + "defi.supply_apy": "APY поставки", + "defi.supply_assets_as_collateral_before_borrowing": "Предоставьте активы в качестве залога перед заимствованием", + "defi.supply_cap_usage": "Использование лимита предложения", + "defi.use_as_collateral": "Использовать в качестве залога", + "defi.utilization_ratio": "Коэффициент использования", + "defi.view_reserve_details": "Просмотреть сведения о резерве", + "defi.with_collateral": "С залогом", + "defi.withdrawable_today": "Доступно для вывода сегодня", "derivation_path": "Путь производного", "description_403": "Наши услуги недоступны в вашем регионе.", "device.btc_only_coming_soon": "Эта функция будет доступна в следующем выпуске прошивки BTC-Only.", @@ -1162,6 +1225,7 @@ "global.approvals": "Подтверждения", "global.approve": "Утвердить", "global.apr": "APR", + "global.apy": "АПИ", "global.asset": "Актив", "global.at_least_variable_characters": "Не менее {variable} символов", "global.auto": "Авто", @@ -1182,6 +1246,7 @@ "global.bluetooth": "Bluetooth", "global.bluetooth_firmware": "Прошивка Bluetooth", "global.bootloader": "Bootloader", + "global.borrow": "Занять", "global.brightness": "Яркость", "global.browser": "Браузер", "global.bulk": "Массовое", @@ -1811,6 +1876,7 @@ "global.wallet": "Кошелек", "global.wallet_activity": "Активность кошелька", "global.wallet_avatar": "Аватар кошелька", + "global.wallet_balance": "Баланс кошелька", "global.wallet_history_notification_banner": "Включите уведомления, чтобы получать мгновенные обновления о действиях в вашем кошельке.", "global.wallets": "Кошельки", "global.wallpaper": "Обои", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "Аутентификация не удалась, проверьте ваше сетевое соединение и попробуйте снова", "ln.payment_received_label": "Платеж получен", "log_out_confirmation_text": "Вы уверены, что хотите выйти из {email}?", + "log_out_wallet": "Выйти из кошелька", + "log_out_wallet_desc": "Это удалит кошелек с этого устройства. Вы можете войти обратно в любое время, используя свою учетную запись в социальной сети и PIN-код.", "logged_out_feedback": "Вы успешно вышли из системы", "login.forgot_passcode": "Забыли код доступа?", "login.forgot_password": "Забыли пароль?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "24ч транзакции", "market.24h_vol_usd": "Объем 24ч (USD)", "market.30d": "30д", + "market.3m": "3м", "market.7d": "7д", "market.add_number_tokens": "Добавить {number} токенов", "market.add_to_favorites": "Добавить в избранное", @@ -3153,8 +3222,11 @@ "send.password_validation": "Пароль должен быть от 8 до 128 символов.", "send.preview_button": "Просмотр", "send.recipient_invalid": "Неверный получатель. Пожалуйста, проверьте и введите заново", + "send.recipient_token_not_activated": "Учетная запись получателя не активировала этот токен. Пожалуйста, попросите получателя добавить токен", "send.send_to_this_address": "Отправить на этот адрес", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "Отправка {0} требует наличия на счету не менее {1} {2}.", + "send.stellar_activation_minimum_hint": "Для активации счета требуется минимум 1 XLM.", + "send.stellar_recipient_account_not_activated": "Счет получателя не активирован в сети Stellar. Пожалуйста, отправьте на этот адрес хотя бы 1 XLM, чтобы инициализировать его.", "send.str_minimum_balance_is_str": "Отправка не удалась. Минимальный баланс для {token} составляет {amount}.", "send.str_minimum_transfer": "Минимальная сумма перевода составляет {0}.", "send.suggest_reserving_str_as_gas_fee": "Предлагаем зарезервировать {0} в качестве сетевого сбора.", diff --git a/packages/shared/src/locale/json/th_TH.json b/packages/shared/src/locale/json/th_TH.json index cb018f35798c..63e23f997510 100644 --- a/packages/shared/src/locale/json/th_TH.json +++ b/packages/shared/src/locale/json/th_TH.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "เต็ม", "Limit.order_status_open": "เปิด", "Limit.order_status_unfilled": "ไม่ได้รับการเติมเต็ม", + "Perps.BBO_Counterparty": "คู่สัญญา 1", + "Perps.BBO_Queue": "คิว 1", + "Perps.BBO_button_desc": "คำสั่ง Best-bid-offer (BBO) จะวางคำสั่งลิมิตที่ราคาซื้อหรือราคาขายที่ดีที่สุด ถ้าคุณใช้โหมด Post-Only คำสั่งของคุณจะถูกส่งเข้าไปก็ต่อเมื่อมันเพิ่มสภาพคล่องเท่านั้น เนื่องจากความผันผวนของตลาด คำสั่งอาจส่งไม่สำเร็จหากราคาตลาดขยับตัวก่อนที่คำสั่งจะได้รับการยืนยัน", + "Perps.BBO_button_title": "最佳买卖价", + "Perps.BBO_select_title": "เลือกโหมดราคาที่ดีที่สุด (BBO)", + "Perps.BBO_unavailable": "ราคาที่ดีที่สุด (BBO) ใช้ไม่ได้", "Perps.referral_bonus_from": "โบนัสแนะนำเพื่อนจากการเทรดบน Perps", "account_model.watched": "ดูแล้ว", "account_name_form_helper_text": "ห้ามป้อนข้อมูลที่ละเอียดอ่อน", @@ -452,9 +458,66 @@ "date.today": "วันนี้", "date.yesterday": "เมื่อวานนี้", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "สินทรัพย์ / ยืมแล้ว", + "defi.asset_can_be_collateral": "สินทรัพย์ / สามารถเป็นหลักประกันได้", + "defi.asset_supplied": "สินทรัพย์ / จัดหาให้แล้ว", + "defi.assets_to_borrow": "สินทรัพย์ที่ให้ยืม", + "defi.assets_to_supply": "สินทรัพย์ที่จัดหาให้", + "defi.available_liquidity": "สภาพคล่องที่มีอยู่", + "defi.available_to_borrow": "พร้อมให้ยืม", + "defi.borrow_apy": "APY การกู้ยืม", + "defi.borrow_cap_usage": "การใช้ขีดจำกัดการกู้ยืม", + "defi.borrowable": "กู้ยืมได้", + "defi.borrowable_today": "กู้ยืมได้วันนี้", + "defi.borrowed": "ยืมแล้ว", + "defi.borrowed_balance": "ยอดคงเหลือที่ยืม", + "defi.can_be_collateral": "สามารถเป็นหลักประกันได้", + "defi.claim_all": "รับทั้งหมด", + "defi.claimable_rewards": "รางวัลที่เรียกร้องได้", + "defi.current_utilization": "การใช้งานปัจจุบัน", + "defi.daily_borrow_cap": "ขีดจำกัดการกู้ยืมรายวัน", + "defi.daily_cap_resets_in": "กำหนดการรีเซ็ตขีดจำกัดรายวันใน", + "defi.daily_caps": "ขีดจำกัดรายวัน", + "defi.daily_withdraw_cap": "ขีดจำกัดการถอนรายวัน", + "defi.from_wallet_balance": "จากยอดเงินในกระเป๋า", + "defi.health_factor": "ปัจจัยสุขภาพ", + "defi.interest_rate_model": "รูปแบบอัตราดอกเบี้ย", "defi.liquidation_acknowledge": "ฉันรับทราบถึงความเสี่ยงที่เกี่ยวข้อง", + "defi.liquidation_at_less_than_1_00": "การชำระบัญชีที่ < 1.00", "defi.liquidation_borrow_desc": "การกู้ยืมเงินจำนวนนี้จะลดปัจจัยด้านสุขภาพของคุณและเพิ่มความเสี่ยงต่อการล้มละลาย", + "defi.liquidation_ltv": "LTV การชำระบัญชี", "defi.liquidation_withdraw_desc": "การถอนเงินจำนวนนี้จะลดปัจจัยด้านสุขภาพของคุณและเพิ่มความเสี่ยงต่อการล้มละลาย", + "defi.manage_position": "จัดการตำแหน่ง", + "defi.max_ltv": "LTV สูงสุด", + "defi.my_borrow": "การกู้ยืมของฉัน", + "defi.my_info": "ข้อมูลของฉัน", + "defi.my_supply": "อุปทานของฉัน", + "defi.net_apy": "APY สุทธิ", + "defi.net_worth": "มูลค่าสุทธิ", + "defi.no_assets_to_borrow": "ไม่มีสินทรัพย์ให้กู้ยืม", + "defi.no_assets_to_supply": "ไม่มีสินทรัพย์พร้อมสำหรับการจัดหา", + "defi.nothing_supplied_yet": "ยังไม่มีการจัดหา", + "defi.oracle_price": "ราคาออราเคิล", + "defi.platform_bonus": "โบนัสแพลตฟอร์ม", + "defi.refundable_fee": "ค่าธรรมเนียมที่ขอคืนได้", + "defi.repay": "ชำระคืน", + "defi.reserve_size": "ขนาดสำรอง", + "defi.safe_max": "ปลอดภัยสูงสุด", + "defi.select_an_asset_to_borrow": "เลือกสินทรัพย์ที่จะกู้ยืม", + "defi.select_an_asset_to_supply": "เลือกสินทรัพย์ที่จะจัดหา", + "defi.show_assets_with_0_balance": "แสดงสินทรัพย์ที่มียอดคงเหลือ 0", + "defi.soft_liquidations": "การชำระบัญชีแบบนุ่มนวล", + "defi.supplied": "จัดหาให้แล้ว", + "defi.supplied_balance": "ยอดคงเหลือที่จัดหา", + "defi.supply": "อุปทาน", + "defi.supply_apy": "APY อุปทาน", + "defi.supply_assets_as_collateral_before_borrowing": "จัดหาสินทรัพย์เป็นหลักประกันก่อนการกู้ยืม", + "defi.supply_cap_usage": "การใช้ขีดจำกัดอุปทาน", + "defi.use_as_collateral": "ใช้เป็นหลักประกัน", + "defi.utilization_ratio": "อัตราการใช้ประโยชน์", + "defi.view_reserve_details": "ดูรายละเอียดเงินสำรอง", + "defi.with_collateral": "พร้อมหลักประกัน", + "defi.withdrawable_today": "ถอนได้วันนี้", "derivation_path": "เส้นทางการสืบทอด", "description_403": "บริการของเราไม่มีให้บริการในภูมิภาคของคุณ", "device.btc_only_coming_soon": "ฟีเจอร์นี้จะพร้อมใช้งานในการอัปเดตเฟิร์มแวร์แบบ BTC-Only รุ่นถัดไป", @@ -1162,6 +1225,7 @@ "global.approvals": "การอนุมัติ", "global.approve": "อนุมัติ", "global.apr": "APR", + "global.apy": "APY", "global.asset": "สินทรัพย์", "global.at_least_variable_characters": "อย่างน้อย {variable} อักขระ", "global.auto": "อัตโนมัติ", @@ -1182,6 +1246,7 @@ "global.bluetooth": "บลูทูธ", "global.bluetooth_firmware": "ฟิร์มแวร์บลูทูธ", "global.bootloader": "Bootloader", + "global.borrow": "ยืม", "global.brightness": "ความสว่าง", "global.browser": "เบราว์เซอร์", "global.bulk": "แบบยกชุด", @@ -1811,6 +1876,7 @@ "global.wallet": "กระเป๋าสตางค์", "global.wallet_activity": "กิจกรรมของกระเป๋าเงิน", "global.wallet_avatar": "อวตารกระเป๋าเงิน", + "global.wallet_balance": "ยอดคงเหลือในกระเป๋า", "global.wallet_history_notification_banner": "เปิดการแจ้งเตือนเพื่อรับข้อมูลอัปเดตเกี่ยวกับกิจกรรมในวอลเล็ตของคุณได้ทันที", "global.wallets": "กระเป๋าสตางค์", "global.wallpaper": "วอลเปเปอร์", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "การยืนยันตัวตนล้มเหลว โปรดตรวจสอบการเชื่อมต่อเครือข่ายของคุณและลองอีกครั้ง", "ln.payment_received_label": "ได้รับการชำระเงินแล้ว", "log_out_confirmation_text": "คุณแน่ใจหรือไม่ว่าต้องการออกจากระบบของ {email}?", + "log_out_wallet": "ออกจากกระเป๋าเงิน", + "log_out_wallet_desc": "การดำเนินการนี้จะลบกระเป๋าเงินออกจากอุปกรณ์นี้ คุณสามารถเข้าสู่ระบบอีกครั้งได้ทุกเมื่อโดยใช้บัญชีโซเชียลและ PIN ของคุณ", "logged_out_feedback": "ออกจากระบบสำเร็จ", "login.forgot_passcode": "ลืมรหัสผ่าน?", "login.forgot_password": "ลืมรหัสผ่าน?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "ธุรกรรม 24 ชั่วโมง", "market.24h_vol_usd": "ปริมาณการซื้อขาย 24 ชั่วโมง (USD)", "market.30d": "30 วัน", + "market.3m": "3 เดือน", "market.7d": "7 วัน", "market.add_number_tokens": "เพิ่ม {number} โทเค็น", "market.add_to_favorites": "เพิ่มลงในรายการโปรด", @@ -3153,8 +3222,11 @@ "send.password_validation": "รหัสผ่านต้องมีความยาวระหว่าง 8 ถึง 128 ตัวอักษร", "send.preview_button": "ดูตัวอย่าง", "send.recipient_invalid": "ผู้รับไม่ถูกต้อง กรุณาตรวจสอบและป้อนใหม่", + "send.recipient_token_not_activated": "บัญชีผู้รับยังไม่ได้เปิดใช้งานโทเค็นนี้ โปรดขอให้ผู้รับเพิ่มโทเค็น", "send.send_to_this_address": "ส่งไปยังที่อยู่นี้", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "การส่ง {0} ต้องมียอดเงินในบัญชีอย่างน้อย {1} {2}.", + "send.stellar_activation_minimum_hint": "ต้องใช้ XLM อย่างน้อย 1 เหรียญในการเปิดใช้งานบัญชี", + "send.stellar_recipient_account_not_activated": "บัญชีผู้รับยังไม่ได้เปิดใช้งานบนเครือข่าย Stellar โปรดส่ง XLM อย่างน้อย 1 เหรียญไปยังที่อยู่นี้เพื่อเริ่มต้นใช้งาน", "send.str_minimum_balance_is_str": "การส่งไม่สำเร็จ ยอดเงินขั้นต่ำสำหรับ {token} คือ {amount}", "send.str_minimum_transfer": "จำนวนเงินที่โอนขั้นต่ำคือ {0}.", "send.suggest_reserving_str_as_gas_fee": "แนะนำให้สำรอง {0} เป็นค่าธรรมเนียมเครือข่าย", diff --git a/packages/shared/src/locale/json/uk_UA.json b/packages/shared/src/locale/json/uk_UA.json index 9db1a06119e2..aa012f15d17a 100644 --- a/packages/shared/src/locale/json/uk_UA.json +++ b/packages/shared/src/locale/json/uk_UA.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "Заповнено", "Limit.order_status_open": "Відкрити", "Limit.order_status_unfilled": "Незаповнений", + "Perps.BBO_Counterparty": "Контрагент 1", + "Perps.BBO_Queue": "Черга 1", + "Perps.BBO_button_desc": "Найкраща ціна купівлі-продажу (BBO) розміщує лімітний ордер за найкращою ціною купівлі або продажу. Якщо ви використовуєте режим «Тільки розміщення», ваш ордер буде розміщено лише в разі додавання ліквідності. Через коливання ринку ордер може не бути розміщений, якщо ринок зміниться до його підтвердження.", + "Perps.BBO_button_title": "BBO", + "Perps.BBO_select_title": "Виберіть режим BBO", + "Perps.BBO_unavailable": "BBO недоступний", "Perps.referral_bonus_from": "Реферальний бонус від торгівлі на Perps", "account_model.watched": "Переглянуто", "account_name_form_helper_text": "Не вводьте конфіденційну інформацію.", @@ -452,9 +458,66 @@ "date.today": "Сьогодні", "date.yesterday": "Вчора", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "Актив / Позичено", + "defi.asset_can_be_collateral": "Актив / Може бути заставою", + "defi.asset_supplied": "Актив / Поставлено", + "defi.assets_to_borrow": "Активи для позики", + "defi.assets_to_supply": "Активи для постачання", + "defi.available_liquidity": "Доступна ліквідність", + "defi.available_to_borrow": "Доступно для запозичення", + "defi.borrow_apy": "APY позики", + "defi.borrow_cap_usage": "Використання ліміту позики", + "defi.borrowable": "Позичабельно", + "defi.borrowable_today": "Доступно для позики сьогодні", + "defi.borrowed": "Позичено", + "defi.borrowed_balance": "Позичений баланс", + "defi.can_be_collateral": "Може бути заставою", + "defi.claim_all": "Забрати все", + "defi.claimable_rewards": "Доступні винагороди", + "defi.current_utilization": "Поточна утилізація", + "defi.daily_borrow_cap": "Денний ліміт позики", + "defi.daily_cap_resets_in": "Щоденний ліміт скидається через", + "defi.daily_caps": "Денні ліміти", + "defi.daily_withdraw_cap": "Денний ліміт зняття", + "defi.from_wallet_balance": "З балансу гаманця", + "defi.health_factor": "Коефіцієнт здоров'я", + "defi.interest_rate_model": "Модель процентної ставки", "defi.liquidation_acknowledge": "Я усвідомлюю пов'язані з цим ризики", + "defi.liquidation_at_less_than_1_00": "Ліквідація при < 1.00", "defi.liquidation_borrow_desc": "Позичання цієї суми зменшить ваш коефіцієнт здоров'я та збільшить ризик ліквідації.", + "defi.liquidation_ltv": "Ліквідаційний LTV", "defi.liquidation_withdraw_desc": "Зняття цієї суми зменшить ваш коефіцієнт здоров'я та збільшить ризик ліквідації.", + "defi.manage_position": "Управління позицією", + "defi.max_ltv": "Максимальний LTV", + "defi.my_borrow": "Моя позика", + "defi.my_info": "Моя інформація", + "defi.my_supply": "Моя пропозиція", + "defi.net_apy": "Чистий APY", + "defi.net_worth": "Чиста вартість", + "defi.no_assets_to_borrow": "Немає активів, доступних для позики", + "defi.no_assets_to_supply": "Немає доступних активів для постачання", + "defi.nothing_supplied_yet": "Поки нічого не поставлено", + "defi.oracle_price": "Ціна оракула", + "defi.platform_bonus": "Бонус платформи", + "defi.refundable_fee": "Поворотна плата", + "defi.repay": "Погасити", + "defi.reserve_size": "Розмір резерву", + "defi.safe_max": "Безпечний максимум", + "defi.select_an_asset_to_borrow": "Виберіть актив для позики", + "defi.select_an_asset_to_supply": "Виберіть актив для постачання", + "defi.show_assets_with_0_balance": "Показати активи з балансом 0", + "defi.soft_liquidations": "М'які ліквідації", + "defi.supplied": "Поставлено", + "defi.supplied_balance": "Поставлений баланс", + "defi.supply": "Пропозиція", + "defi.supply_apy": "APY постачання", + "defi.supply_assets_as_collateral_before_borrowing": "Надайте активи як заставу перед позикою", + "defi.supply_cap_usage": "Використання ліміту пропозиції", + "defi.use_as_collateral": "Використовувати як заставу", + "defi.utilization_ratio": "Коефіцієнт використання", + "defi.view_reserve_details": "Переглянути деталі резерву", + "defi.with_collateral": "З заставою", + "defi.withdrawable_today": "Доступно для виведення сьогодні", "derivation_path": "Шлях похідного", "description_403": "Наші послуги недоступні у вашому регіоні.", "device.btc_only_coming_soon": "Ця функція буде доступна в наступному випуску прошивки BTC-Only.", @@ -1162,6 +1225,7 @@ "global.approvals": "Підтвердження", "global.approve": "Підтвердити", "global.apr": "APR", + "global.apy": "APY", "global.asset": "Актив", "global.at_least_variable_characters": "Щонайменше {variable} символів", "global.auto": "Авто", @@ -1182,6 +1246,7 @@ "global.bluetooth": "Bluetooth", "global.bluetooth_firmware": "Прошивка Bluetooth", "global.bootloader": "Bootloader", + "global.borrow": "Позичити", "global.brightness": "Яскравість", "global.browser": "Браузер", "global.bulk": "Масово", @@ -1811,6 +1876,7 @@ "global.wallet": "Гаманець", "global.wallet_activity": "Активність гаманця", "global.wallet_avatar": "Аватар гаманця", + "global.wallet_balance": "Баланс гаманця", "global.wallet_history_notification_banner": "Увімкніть сповіщення, щоб миттєво отримувати оновлення про активність вашого гаманця.", "global.wallets": "Гаманці", "global.wallpaper": "Шпалери", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "Помилка аутентифікації, перевірте ваше мережеве з'єднання та спробуйте знову", "ln.payment_received_label": "Отриманої компенсації", "log_out_confirmation_text": "Ви впевнені, що хочете вийти з облікового запису {email}?", + "log_out_wallet": "Вийти з гаманця", + "log_out_wallet_desc": "Це видалить гаманець з цього пристрою. Ви можете увійти знову в будь-який час, використовуючи свій обліковий запис у соціальній мережі та PIN-код.", "logged_out_feedback": "Вихід виконано успішно", "login.forgot_passcode": "Забули код доступу?", "login.forgot_password": "Забули пароль?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "24г транзакції", "market.24h_vol_usd": "Обсяг 24г (USD)", "market.30d": "30д", + "market.3m": "3м", "market.7d": "7д", "market.add_number_tokens": "Додати {number} токенів", "market.add_to_favorites": "Додати до обраного", @@ -3153,8 +3222,11 @@ "send.password_validation": "Пароль повинен містити від 8 до 128 символів.", "send.preview_button": "Попередній перегляд", "send.recipient_invalid": "Неправильний отримувач. Будь ласка, перевірте та введіть знову", + "send.recipient_token_not_activated": "Обліковий запис одержувача не активував цей токен. Попросіть одержувача додати токен", "send.send_to_this_address": "Надіслати на цю адресу", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "Для відправлення {0} потрібно мати на балансі рахунку принаймні {1} {2}.", + "send.stellar_activation_minimum_hint": "Для активації облікового запису потрібно мінімум 1 XLM.", + "send.stellar_recipient_account_not_activated": "Обліковий запис одержувача не активовано в мережі Stellar. Будь ласка, надішліть принаймні 1 XLM на цю адресу для його ініціалізації.", "send.str_minimum_balance_is_str": "Невдала спроба відправки. Мінімальний баланс для {token} становить {amount}.", "send.str_minimum_transfer": "Мінімальна сума переказу становить {0}.", "send.suggest_reserving_str_as_gas_fee": "Пропонуємо зарезервувати {0} як мережевий збір.", diff --git a/packages/shared/src/locale/json/vi.json b/packages/shared/src/locale/json/vi.json index 9b43530c1cae..9237c80524c0 100644 --- a/packages/shared/src/locale/json/vi.json +++ b/packages/shared/src/locale/json/vi.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "Đã điền", "Limit.order_status_open": "Mở", "Limit.order_status_unfilled": "Chưa thực hiện", + "Perps.BBO_Counterparty": "Đối tác 1", + "Perps.BBO_Queue": "Hàng đợi 1", + "Perps.BBO_button_desc": "Giá mua-bán tốt nhất (BBO) đặt lệnh giới hạn ở mức giá mua hoặc bán tốt nhất. Nếu bạn sử dụng Chỉ Đăng, lệnh của bạn sẽ chỉ được đặt nếu nó tăng thêm thanh khoản. Do biến động thị trường, lệnh có thể không được đặt nếu thị trường di chuyển trước khi được xác nhận.", + "Perps.BBO_button_title": "BBO", + "Perps.BBO_select_title": "Chọn Chế độ BBO", + "Perps.BBO_unavailable": "BBO không khả dụng", "Perps.referral_bonus_from": "Tiền thưởng giới thiệu từ giao dịch trên Perps", "account_model.watched": "Đã xem", "account_name_form_helper_text": "Không nhập thông tin nhạy cảm.", @@ -452,9 +458,66 @@ "date.today": "Hôm nay", "date.yesterday": "Hôm qua", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "Tài sản / Đã vay", + "defi.asset_can_be_collateral": "Tài sản / Có thể làm tài sản thế chấp", + "defi.asset_supplied": "Tài sản / Đã cung cấp", + "defi.assets_to_borrow": "Tài sản để vay", + "defi.assets_to_supply": "Tài sản để cung cấp", + "defi.available_liquidity": "Thanh khoản khả dụng", + "defi.available_to_borrow": "Có sẵn để vay", + "defi.borrow_apy": "APY cho vay", + "defi.borrow_cap_usage": "Sử dụng giới hạn vay", + "defi.borrowable": "Có thể vay", + "defi.borrowable_today": "Có thể vay hôm nay", + "defi.borrowed": "Đã vay", + "defi.borrowed_balance": "Số dư đã vay", + "defi.can_be_collateral": "Có thể là tài sản thế chấp", + "defi.claim_all": "Nhận tất cả", + "defi.claimable_rewards": "Phần thưởng có thể nhận", + "defi.current_utilization": "Sử dụng hiện tại", + "defi.daily_borrow_cap": "Giới hạn vay hàng ngày", + "defi.daily_cap_resets_in": "Giới hạn hàng ngày được đặt lại sau", + "defi.daily_caps": "Giới hạn hàng ngày", + "defi.daily_withdraw_cap": "Giới hạn rút tiền hàng ngày", + "defi.from_wallet_balance": "Từ số dư ví", + "defi.health_factor": "Hệ số sức khỏe", + "defi.interest_rate_model": "Mô hình lãi suất", "defi.liquidation_acknowledge": "Tôi thừa nhận những rủi ro liên quan.", + "defi.liquidation_at_less_than_1_00": "Thanh lý tại < 1.00", "defi.liquidation_borrow_desc": "Vay số tiền này sẽ làm giảm hệ số sức khỏe của bạn và tăng nguy cơ bị thanh lý.", + "defi.liquidation_ltv": "LTV thanh lý", "defi.liquidation_withdraw_desc": "Việc rút số tiền này sẽ làm giảm hệ số sức khỏe của bạn và tăng nguy cơ bị thanh lý.", + "defi.manage_position": "Quản lý vị thế", + "defi.max_ltv": "LTV tối đa", + "defi.my_borrow": "Khoản vay của tôi", + "defi.my_info": "Thông tin của tôi", + "defi.my_supply": "Cung cấp của tôi", + "defi.net_apy": "APY ròng", + "defi.net_worth": "Giá trị ròng", + "defi.no_assets_to_borrow": "Không có tài sản nào để vay mượn", + "defi.no_assets_to_supply": "Không có tài sản nào để cung cấp.", + "defi.nothing_supplied_yet": "Chưa có gì được cung cấp", + "defi.oracle_price": "Giá Oracle", + "defi.platform_bonus": "Thưởng nền tảng", + "defi.refundable_fee": "Phí hoàn lại", + "defi.repay": "Hoàn trả", + "defi.reserve_size": "Kích thước dự trữ", + "defi.safe_max": "Tối đa an toàn", + "defi.select_an_asset_to_borrow": "Chọn một tài sản để vay", + "defi.select_an_asset_to_supply": "Chọn một tài sản để cung cấp", + "defi.show_assets_with_0_balance": "Hiển thị tài sản có số dư 0", + "defi.soft_liquidations": "Thanh lý mềm", + "defi.supplied": "Đã cung cấp", + "defi.supplied_balance": "Số dư đã cung cấp", + "defi.supply": "Cung cấp", + "defi.supply_apy": "APY cung cấp", + "defi.supply_assets_as_collateral_before_borrowing": "Cung cấp tài sản làm tài sản thế chấp trước khi vay", + "defi.supply_cap_usage": "Sử dụng giới hạn cung cấp", + "defi.use_as_collateral": "Sử dụng làm tài sản thế chấp", + "defi.utilization_ratio": "Tỷ lệ sử dụng", + "defi.view_reserve_details": "Xem chi tiết dự trữ", + "defi.with_collateral": "Kèm theo tài sản thế chấp", + "defi.withdrawable_today": "Có thể rút hôm nay", "derivation_path": "Đường dẫn xuất phát", "description_403": "Dịch vụ của chúng tôi không có sẵn ở khu vực của bạn.", "device.btc_only_coming_soon": "Tính năng này sẽ có sẵn trong bản phát hành firmware BTC-Only sau này.", @@ -1162,6 +1225,7 @@ "global.approvals": "Phê duyệt", "global.approve": "Chấp nhận", "global.apr": "APR", + "global.apy": "APY", "global.asset": "Tài sản", "global.at_least_variable_characters": "Ít nhất {variable} ký tự", "global.auto": "Tự động", @@ -1182,6 +1246,7 @@ "global.bluetooth": "Bluetooth", "global.bluetooth_firmware": "Phần mềm cứng Bluetooth", "global.bootloader": "Bootloader", + "global.borrow": "Vay", "global.brightness": "Độ sáng", "global.browser": "Trình duyệt", "global.bulk": "Hàng loạt", @@ -1811,6 +1876,7 @@ "global.wallet": "Ví", "global.wallet_activity": "Hoạt động ví", "global.wallet_avatar": "Ảnh đại diện ví", + "global.wallet_balance": "Số dư ví", "global.wallet_history_notification_banner": "Bật thông báo để nhận cập nhật ngay lập tức về hoạt động ví của bạn.", "global.wallets": "Ví", "global.wallpaper": "Hình nền", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "Xác thực không thành công, kiểm tra kết nối mạng của bạn và thử lại", "ln.payment_received_label": "Thanh toán nhận được", "log_out_confirmation_text": "Bạn có chắc muốn đăng xuất khỏi {email} không?", + "log_out_wallet": "Đăng xuất ví", + "log_out_wallet_desc": "Thao tác này sẽ xóa ví khỏi thiết bị này. Bạn có thể đăng nhập lại bất cứ lúc nào bằng tài khoản mạng xã hội và mã PIN của mình.", "logged_out_feedback": "Đăng xuất thành công", "login.forgot_passcode": "Quên mã khóa?", "login.forgot_password": "Quên mật khẩu?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "Giao dịch 24 giờ", "market.24h_vol_usd": "Khối Lượng 24 giờ (USD)", "market.30d": "30ng", + "market.3m": "3th", "market.7d": "7ng", "market.add_number_tokens": "Thêm {number} mã thông báo", "market.add_to_favorites": "Thêm vào mục yêu thích", @@ -3153,8 +3222,11 @@ "send.password_validation": "Mật khẩu phải từ 8 đến 128 ký tự.", "send.preview_button": "Xem trước", "send.recipient_invalid": "Người nhận không hợp lệ. Vui lòng kiểm tra và nhập lại", + "send.recipient_token_not_activated": "Tài khoản người nhận chưa kích hoạt mã thông báo này. Vui lòng yêu cầu người nhận thêm mã thông báo.", "send.send_to_this_address": "Gửi đến địa chỉ này", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "Gửi {0} yêu cầu số dư tài khoản tối thiểu là {1} {2}.", + "send.stellar_activation_minimum_hint": "Cần tối thiểu 1 XLM để kích hoạt tài khoản.", + "send.stellar_recipient_account_not_activated": "Tài khoản người nhận chưa được kích hoạt trên mạng Stellar. Vui lòng gửi ít nhất 1 XLM đến địa chỉ này để kích hoạt tài khoản.", "send.str_minimum_balance_is_str": "Gửi thất bại. Số dư tối thiểu cho {token} là {amount}.", "send.str_minimum_transfer": "Số tiền chuyển tối thiểu là {0}.", "send.suggest_reserving_str_as_gas_fee": "Đề xuất dành {0} làm phí mạng.", diff --git a/packages/shared/src/locale/json/zh_CN.json b/packages/shared/src/locale/json/zh_CN.json index ecd243e0a1d5..2f7675229ee9 100644 --- a/packages/shared/src/locale/json/zh_CN.json +++ b/packages/shared/src/locale/json/zh_CN.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "已成交", "Limit.order_status_open": "进行中", "Limit.order_status_unfilled": "未成交", + "Perps.BBO_Counterparty": "对手价 1", + "Perps.BBO_Queue": "同向价 1", + "Perps.BBO_button_desc": "最优价(BBO)以最优买价或卖价下限价单。如果您使用只做挂单,您的订单只有在增加流动性时才会被下单。由于市场波动,如果市场在确认前发生变动,订单可能无法下单。", + "Perps.BBO_button_title": "最优价", + "Perps.BBO_select_title": "选择最优价模式", + "Perps.BBO_unavailable": "最优价不可用", "Perps.referral_bonus_from": "在 Perps 上交易的返佣奖金", "account_model.watched": "观察账户", "account_name_form_helper_text": "请勿输入机密信息。", @@ -452,9 +458,66 @@ "date.today": "今天", "date.yesterday": "昨天", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "资产 / 已借入", + "defi.asset_can_be_collateral": "资产 / 可作为抵押品", + "defi.asset_supplied": "资产 / 已存入", + "defi.assets_to_borrow": "可借资产", + "defi.assets_to_supply": "可存入资产", + "defi.available_liquidity": "可用流动性", + "defi.available_to_borrow": "可借", + "defi.borrow_apy": "借款 APY", + "defi.borrow_cap_usage": "借款上限使用", + "defi.borrowable": "可借款", + "defi.borrowable_today": "今日可借", + "defi.borrowed": "已借入", + "defi.borrowed_balance": "借入余额", + "defi.can_be_collateral": "可用作抵押品", + "defi.claim_all": "全部领取", + "defi.claimable_rewards": "可领取奖励", + "defi.current_utilization": "当前利用率", + "defi.daily_borrow_cap": "每日借款上限", + "defi.daily_cap_resets_in": "每日上限重置于", + "defi.daily_caps": "每日上限", + "defi.daily_withdraw_cap": "每日提领上限", + "defi.from_wallet_balance": "从钱包余额", + "defi.health_factor": "健康系数", + "defi.interest_rate_model": "利率模型", "defi.liquidation_acknowledge": "我了解其中存在的风险。", + "defi.liquidation_at_less_than_1_00": "清算 < 1.00", "defi.liquidation_borrow_desc": "借入这笔款项会降低您的健康系数,并增加破产清算的风险。", + "defi.liquidation_ltv": "清算 LTV", "defi.liquidation_withdraw_desc": "提取这笔款项会降低您的健康系数,并增加破产清算的风险。", + "defi.manage_position": "管理仓位", + "defi.max_ltv": "最高 LTV", + "defi.my_borrow": "我的借款", + "defi.my_info": "我的信息", + "defi.my_supply": "我的存入", + "defi.net_apy": "净 APY", + "defi.net_worth": "净值", + "defi.no_assets_to_borrow": "没有可供借贷的资产", + "defi.no_assets_to_supply": "没有可供存入的资产", + "defi.nothing_supplied_yet": "暂无存入", + "defi.oracle_price": "预言机价格", + "defi.platform_bonus": "平台奖励", + "defi.refundable_fee": "可退还费用", + "defi.repay": "偿还", + "defi.reserve_size": "储备规模", + "defi.safe_max": "安全上限", + "defi.select_an_asset_to_borrow": "选择要借的资产", + "defi.select_an_asset_to_supply": "选择要存入的资产", + "defi.show_assets_with_0_balance": "显示余额为 0 的资产", + "defi.soft_liquidations": "软清算", + "defi.supplied": "已存入", + "defi.supplied_balance": "存入余额", + "defi.supply": "存入", + "defi.supply_apy": "存入 APY", + "defi.supply_assets_as_collateral_before_borrowing": "借款前请先提供抵押资产", + "defi.supply_cap_usage": "存入上限使用", + "defi.use_as_collateral": "用作抵押", + "defi.utilization_ratio": "利用率", + "defi.view_reserve_details": "查看储备详情", + "defi.with_collateral": "使用抵押品", + "defi.withdrawable_today": "今日可提领", "derivation_path": "派生路径", "description_403": "我们的服务在您的地区无法使用", "device.btc_only_coming_soon": "此功能将在后续的 BTC-Only 固件上线后提供。", @@ -1162,6 +1225,7 @@ "global.approvals": "授权", "global.approve": "授权", "global.apr": "APR", + "global.apy": "APY", "global.asset": "资产", "global.at_least_variable_characters": "至少 {variable} 个字符", "global.auto": "自动", @@ -1182,6 +1246,7 @@ "global.bluetooth": "蓝牙", "global.bluetooth_firmware": "蓝牙固件", "global.bootloader": "Bootloader", + "global.borrow": "借币", "global.brightness": "亮度", "global.browser": "浏览器", "global.bulk": "批量", @@ -1811,6 +1876,7 @@ "global.wallet": "钱包", "global.wallet_activity": "钱包活动", "global.wallet_avatar": "钱包头像", + "global.wallet_balance": "钱包余额", "global.wallet_history_notification_banner": "启用通知以即时获取您的钱包活动更新。", "global.wallets": "钱包", "global.wallpaper": "壁纸", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "身份认证失败,请检查网络连接后重试", "ln.payment_received_label": "已收到付款", "log_out_confirmation_text": "您确定要退出 {email} 的登录吗?", + "log_out_wallet": "退出钱包", + "log_out_wallet_desc": "这将从当前设备移除钱包。你可以随时使用社交账号和 PIN 重新登录。", "logged_out_feedback": "已成功退出登录", "login.forgot_passcode": "忘记密码?", "login.forgot_password": "忘记密码?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "24 小时交易", "market.24h_vol_usd": "24 小时成交量 (USD)", "market.30d": "30 天", + "market.3m": "3 月", "market.7d": "7 天", "market.add_number_tokens": "添加 {number} 个代币", "market.add_to_favorites": "添加到自选", @@ -3153,8 +3222,11 @@ "send.password_validation": "密码必须在 8 到 128 个字符之间", "send.preview_button": "预览", "send.recipient_invalid": "无效的收件人。请检查并重新输入", + "send.recipient_token_not_activated": "接收方账户尚未激活该代币,请提醒对方添加 Token", "send.send_to_this_address": "发送到此地址", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "发送 {0} 需要账户余额至少为 {1} {2}", + "send.stellar_activation_minimum_hint": "激活该账户至少需要发送 1 XLM。", + "send.stellar_recipient_account_not_activated": "接收方账户尚未在 Stellar 网络激活,请先向该地址发送至少 1 XLM 以完成初始化。", "send.str_minimum_balance_is_str": "发送失败,{token} 的最低余额为 {amount}", "send.str_minimum_transfer": "最少转账数量 {0}", "send.suggest_reserving_str_as_gas_fee": "建议预留 {0} 作为网络费用", diff --git a/packages/shared/src/locale/json/zh_HK.json b/packages/shared/src/locale/json/zh_HK.json index cf7bc3bfba61..44d5acb2a9d5 100644 --- a/packages/shared/src/locale/json/zh_HK.json +++ b/packages/shared/src/locale/json/zh_HK.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "已成交", "Limit.order_status_open": "進行中", "Limit.order_status_unfilled": "未成交", + "Perps.BBO_Counterparty": "對手價 1", + "Perps.BBO_Queue": "同向價 1", + "Perps.BBO_button_desc": "最優價 (BBO) 會以最佳買入價或賣出價下限價單。如果您使用只掛單模式,您的訂單只會在增加流動性時才會被下單。由於市場波動,如果市場在確認前發生變動,訂單可能會下單失敗。", + "Perps.BBO_button_title": "最優價", + "Perps.BBO_select_title": "選擇最優價模式", + "Perps.BBO_unavailable": "最優價不可用", "Perps.referral_bonus_from": "在 Perps 上交易的推薦獎金", "account_model.watched": "觀察帳戶", "account_name_form_helper_text": "請勿輸入敏感資料。", @@ -452,9 +458,66 @@ "date.today": "今天", "date.yesterday": "昨天", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "資產 / 已借入", + "defi.asset_can_be_collateral": "資產 / 可作為抵押品", + "defi.asset_supplied": "資產 / 已存入", + "defi.assets_to_borrow": "可借資產", + "defi.assets_to_supply": "可存入資產", + "defi.available_liquidity": "可用流動性", + "defi.available_to_borrow": "可借", + "defi.borrow_apy": "借款 APY", + "defi.borrow_cap_usage": "借款上限使用", + "defi.borrowable": "可借款", + "defi.borrowable_today": "今日可借", + "defi.borrowed": "已借入", + "defi.borrowed_balance": "借入餘額", + "defi.can_be_collateral": "可用作抵押品", + "defi.claim_all": "全部領取", + "defi.claimable_rewards": "可領取獎勵", + "defi.current_utilization": "當前利用率", + "defi.daily_borrow_cap": "每日借款上限", + "defi.daily_cap_resets_in": "每日上限重置於", + "defi.daily_caps": "每日上限", + "defi.daily_withdraw_cap": "每日提領上限", + "defi.from_wallet_balance": "從錢包餘額", + "defi.health_factor": "健康係數", + "defi.interest_rate_model": "利率模型", "defi.liquidation_acknowledge": "我了解其中存在的風險。", + "defi.liquidation_at_less_than_1_00": "清算 < 1.00", "defi.liquidation_borrow_desc": "借入這筆款項會降低您的健康係數,並增加破產清算的風險。", + "defi.liquidation_ltv": "清算 LTV", "defi.liquidation_withdraw_desc": "提取這筆款項會降低您的健康係數,並增加破產清算的風險。", + "defi.manage_position": "管理倉位", + "defi.max_ltv": "最高 LTV", + "defi.my_borrow": "我的借款", + "defi.my_info": "我的資訊", + "defi.my_supply": "我的存入", + "defi.net_apy": "淨 APY", + "defi.net_worth": "淨值", + "defi.no_assets_to_borrow": "沒有可供借貸的資產", + "defi.no_assets_to_supply": "沒有可供存入的資產", + "defi.nothing_supplied_yet": "暫無存入", + "defi.oracle_price": "預言機價格", + "defi.platform_bonus": "平台獎勵", + "defi.refundable_fee": "可退還費用", + "defi.repay": "償還", + "defi.reserve_size": "儲備規模", + "defi.safe_max": "安全上限", + "defi.select_an_asset_to_borrow": "選擇要借的資產", + "defi.select_an_asset_to_supply": "選擇要存入的資產", + "defi.show_assets_with_0_balance": "顯示餘額為 0 的資產", + "defi.soft_liquidations": "軟清算", + "defi.supplied": "已存入", + "defi.supplied_balance": "存入餘額", + "defi.supply": "存入", + "defi.supply_apy": "存入 APY", + "defi.supply_assets_as_collateral_before_borrowing": "借款前請先提供抵押資產", + "defi.supply_cap_usage": "存入上限使用", + "defi.use_as_collateral": "用作抵押", + "defi.utilization_ratio": "利用率", + "defi.view_reserve_details": "查看儲備詳情", + "defi.with_collateral": "使用抵押品", + "defi.withdrawable_today": "今日可提領", "derivation_path": "派生路徑", "description_403": "我們的服務在您的地區並不提供", "device.btc_only_coming_soon": "此功能將在稍後的 BTC-Only 韌體版本中提供。", @@ -1162,6 +1225,7 @@ "global.approvals": "授權", "global.approve": "授權", "global.apr": "APR", + "global.apy": "APY", "global.asset": "資產", "global.at_least_variable_characters": "至少 {variable} 個字元", "global.auto": "自動", @@ -1182,6 +1246,7 @@ "global.bluetooth": "藍牙", "global.bluetooth_firmware": "藍牙固件", "global.bootloader": "Bootloader", + "global.borrow": "借幣", "global.brightness": "亮度", "global.browser": "瀏覽器", "global.bulk": "批次", @@ -1811,6 +1876,7 @@ "global.wallet": "錢包", "global.wallet_activity": "錢包活動", "global.wallet_avatar": "錢包頭像", + "global.wallet_balance": "錢包餘額", "global.wallet_history_notification_banner": "啟用通知以即時獲取您錢包活動的更新。", "global.wallets": "錢包", "global.wallpaper": "壁紙", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "身份驗證失敗,請檢查網絡連接後重試。", "ln.payment_received_label": "已收到付款", "log_out_confirmation_text": "您確定要登出 {email} 嗎?", + "log_out_wallet": "登出錢包", + "log_out_wallet_desc": "這將從當前裝置移除錢包。你可以隨時使用社群帳號和 PIN 重新登入。", "logged_out_feedback": "已成功登出", "login.forgot_passcode": "忘記密碼?", "login.forgot_password": "忘記密碼?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "24 小時交易", "market.24h_vol_usd": "24 小時成交量 (USD)", "market.30d": "30 日", + "market.3m": "3 月", "market.7d": "7 天", "market.add_number_tokens": "添加 {number} 個代幣", "market.add_to_favorites": "添加到自選", @@ -3153,8 +3222,11 @@ "send.password_validation": "密碼必須介於 8 到 128 個字元之間", "send.preview_button": "預覽", "send.recipient_invalid": "無效的收件人。請檢查並重新輸入。", + "send.recipient_token_not_activated": "收件者帳戶尚未啟動此令牌。請收件者新增令牌。", "send.send_to_this_address": "發送到此地址", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "發送 {0} 需要帳戶餘額至少為{1} {2}", + "send.stellar_activation_minimum_hint": "啟動帳戶至少需要 1 個 XLM。", + "send.stellar_recipient_account_not_activated": "收款帳戶尚未在Stellar網路上啟用。請向此位址發送至少1個XLM以進行初始化。", "send.str_minimum_balance_is_str": "發送失敗,{token} 的最低餘額為 {amount}", "send.str_minimum_transfer": "最少轉賬數量 {0}", "send.suggest_reserving_str_as_gas_fee": "建議保留 {0} 作為網路費用", diff --git a/packages/shared/src/locale/json/zh_TW.json b/packages/shared/src/locale/json/zh_TW.json index 196f552421cd..070721805864 100644 --- a/packages/shared/src/locale/json/zh_TW.json +++ b/packages/shared/src/locale/json/zh_TW.json @@ -37,6 +37,12 @@ "Limit.order_status_filled": "已成交", "Limit.order_status_open": "進行中", "Limit.order_status_unfilled": "未成交", + "Perps.BBO_Counterparty": "對手價 1", + "Perps.BBO_Queue": "同向價 1", + "Perps.BBO_button_desc": "最優價(BBO)會以最佳買價或賣價下限價單。如果您使用只掛單,您的訂單只會在增加流動性時才會被下單。由於市場波動,如果市場在確認前發生變動,訂單可能會下單失敗。", + "Perps.BBO_button_title": "最優價", + "Perps.BBO_select_title": "選擇最優價模式", + "Perps.BBO_unavailable": "最優價不可用", "Perps.referral_bonus_from": "在 Perps 上交易的推薦獎金", "account_model.watched": "觀察帳戶", "account_name_form_helper_text": "請勿輸入敏感資訊。", @@ -452,9 +458,66 @@ "date.today": "今天", "date.yesterday": "昨天", "defi.apr_apy": "APR/APY", + "defi.asset_borrowed": "資產 / 已借入", + "defi.asset_can_be_collateral": "資產 / 可作為抵押品", + "defi.asset_supplied": "資產 / 已存入", + "defi.assets_to_borrow": "可借資產", + "defi.assets_to_supply": "可存入資產", + "defi.available_liquidity": "可用流動性", + "defi.available_to_borrow": "可借", + "defi.borrow_apy": "借款 APY", + "defi.borrow_cap_usage": "借款上限使用", + "defi.borrowable": "可借款", + "defi.borrowable_today": "今日可借", + "defi.borrowed": "已借入", + "defi.borrowed_balance": "借入餘額", + "defi.can_be_collateral": "可用作抵押品", + "defi.claim_all": "全部領取", + "defi.claimable_rewards": "可領取獎勵", + "defi.current_utilization": "當前利用率", + "defi.daily_borrow_cap": "每日借款上限", + "defi.daily_cap_resets_in": "每日上限重置於", + "defi.daily_caps": "每日上限", + "defi.daily_withdraw_cap": "每日提領上限", + "defi.from_wallet_balance": "從錢包餘額", + "defi.health_factor": "健康係數", + "defi.interest_rate_model": "利率模型", "defi.liquidation_acknowledge": "我了解其中存在的風險。", + "defi.liquidation_at_less_than_1_00": "清算 < 1.00", "defi.liquidation_borrow_desc": "借入這筆款項會降低您的健康係數,並增加破產清算的風險。", + "defi.liquidation_ltv": "清算 LTV", "defi.liquidation_withdraw_desc": "提取這筆款項會降低您的健康係數,並增加破產清算的風險。", + "defi.manage_position": "管理倉位", + "defi.max_ltv": "最高 LTV", + "defi.my_borrow": "我的借款", + "defi.my_info": "我的資訊", + "defi.my_supply": "我的存入", + "defi.net_apy": "淨 APY", + "defi.net_worth": "淨值", + "defi.no_assets_to_borrow": "沒有可供借貸的資產", + "defi.no_assets_to_supply": "沒有可供存入的資產", + "defi.nothing_supplied_yet": "暫無存入", + "defi.oracle_price": "預言機價格", + "defi.platform_bonus": "平台獎勵", + "defi.refundable_fee": "可退還費用", + "defi.repay": "償還", + "defi.reserve_size": "儲備規模", + "defi.safe_max": "安全上限", + "defi.select_an_asset_to_borrow": "選擇要借的資產", + "defi.select_an_asset_to_supply": "選擇要存入的資產", + "defi.show_assets_with_0_balance": "顯示餘額為 0 的資產", + "defi.soft_liquidations": "軟清算", + "defi.supplied": "已存入", + "defi.supplied_balance": "存入餘額", + "defi.supply": "存入", + "defi.supply_apy": "存入 APY", + "defi.supply_assets_as_collateral_before_borrowing": "借款前請先提供抵押資產", + "defi.supply_cap_usage": "存入上限使用", + "defi.use_as_collateral": "用作抵押", + "defi.utilization_ratio": "利用率", + "defi.view_reserve_details": "查看儲備詳情", + "defi.with_collateral": "使用抵押品", + "defi.withdrawable_today": "今日可提領", "derivation_path": "導出路徑", "description_403": "我們的服務在您的地區無法使用", "device.btc_only_coming_soon": "此功能將在稍後的 BTC-Only 韌體版本中提供。", @@ -1162,6 +1225,7 @@ "global.approvals": "授權", "global.approve": "授權", "global.apr": "APR", + "global.apy": "APY", "global.asset": "資產", "global.at_least_variable_characters": "至少 {variable} 個字元", "global.auto": "自動", @@ -1182,6 +1246,7 @@ "global.bluetooth": "藍牙", "global.bluetooth_firmware": "藍牙韌體", "global.bootloader": "Bootloader", + "global.borrow": "借幣", "global.brightness": "亮度", "global.browser": "瀏覽器", "global.bulk": "批次", @@ -1811,6 +1876,7 @@ "global.wallet": "錢包", "global.wallet_activity": "錢包活動", "global.wallet_avatar": "錢包頭像", + "global.wallet_balance": "錢包餘額", "global.wallet_history_notification_banner": "啟用通知以即時獲取您錢包活動的更新。", "global.wallets": "錢包", "global.wallpaper": "壁紙", @@ -2015,6 +2081,8 @@ "ln.authorize_access_network_error": "驗證失敗,請檢查您的網路連線並再試一次", "ln.payment_received_label": "已收到付款", "log_out_confirmation_text": "您確定要登出 {email} 嗎?", + "log_out_wallet": "登出錢包", + "log_out_wallet_desc": "這將從當前裝置移除錢包。你可以隨時使用社群帳號和 PIN 重新登入。", "logged_out_feedback": "已成功登出", "login.forgot_passcode": "忘記密碼?", "login.forgot_password": "忘記密碼?", @@ -2051,6 +2119,7 @@ "market.24h_txns": "24 小時交易", "market.24h_vol_usd": "24 小時成交量 (USD)", "market.30d": "30 天", + "market.3m": "3 月", "market.7d": "7 天", "market.add_number_tokens": "添加 {number} 個代幣", "market.add_to_favorites": "添加到自選", @@ -3153,8 +3222,11 @@ "send.password_validation": "密碼必須介於 8 到 128 個字符之間", "send.preview_button": "預覽", "send.recipient_invalid": "無效的收件人。請檢查並重新輸入", + "send.recipient_token_not_activated": "收件者帳戶尚未啟動此令牌。請收件者新增令牌。", "send.send_to_this_address": "發送到此地址", "send.sending_str_requires_an_account_balance_of_at_least_str_str": "發送 {0} 需要帳戶餘額至少為 {1} {2}", + "send.stellar_activation_minimum_hint": "啟動帳戶至少需要 1 個 XLM。", + "send.stellar_recipient_account_not_activated": "收款帳戶尚未在Stellar網路上啟用。請向此位址發送至少1個XLM以進行初始化。", "send.str_minimum_balance_is_str": "發送失敗,{token} 的最低餘額為 {amount}", "send.str_minimum_transfer": "最少轉賬數量 {0}", "send.suggest_reserving_str_as_gas_fee": "建議保留 {0} 作為網路費用", From 91d9fb299075141ef9ec8d854969882b3c229690 Mon Sep 17 00:00:00 2001 From: Franco Date: Tue, 30 Dec 2025 17:04:13 +0800 Subject: [PATCH 42/66] fix badge font size --- .../components/WalletEdit/BulkCopyAddressesButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/BulkCopyAddressesButton.tsx b/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/BulkCopyAddressesButton.tsx index 217bd3bd884d..6abefcb4d498 100644 --- a/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/BulkCopyAddressesButton.tsx +++ b/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/BulkCopyAddressesButton.tsx @@ -130,7 +130,7 @@ export function BulkCopyAddressesButton({ extra={ isPrimeUser ? null : ( - + {intl.formatMessage({ id: ETranslations.prime_status_prime, })} From 16af490e861f3ff7577b8343f2ef3be5b1c8c53d Mon Sep 17 00:00:00 2001 From: morizon Date: Tue, 30 Dec 2025 17:52:20 +0800 Subject: [PATCH 43/66] feat: enhance Keyless Wallet onboarding flow by adding refresh token handling and improving navigation logic; refactor related components for better state management and user experience --- packages/kit-bg/src/dbs/local/LocalDbBase.ts | 24 ++ packages/kit-bg/src/dbs/local/types.ts | 8 + .../services/ServiceAccount/ServiceAccount.ts | 19 ++ .../ServiceKeylessWallet.ts | 175 ++++++++++++- .../utils/keylessRefreshTokenStorage.ts | 137 ++++++++++ .../KeylessWallet/useKeylessWallet.tsx | 244 +++++++++++++----- .../contexts/accountSelector/actions.tsx | 3 + .../WalletEdit/WalletEditButton.tsx | 4 +- .../pages/FinalizeWalletSetup.tsx | 2 + .../Onboardingv2/pages/NewPinCreatedPage.tsx | 2 +- .../Onboardingv2/pages/OneKeyIDLoginPage.tsx | 13 +- .../Setting/pages/Tab/SettingListSubModal.tsx | 17 +- .../src/views/Setting/pages/Tab/config.tsx | 2 +- .../src/keylessWallet/keylessWalletTypes.ts | 1 + packages/shared/src/routes/onboardingv2.ts | 2 + packages/shared/src/utils/accountUtils.ts | 9 + 16 files changed, 579 insertions(+), 83 deletions(-) create mode 100644 packages/kit-bg/src/services/ServiceKeylessWallet/utils/keylessRefreshTokenStorage.ts diff --git a/packages/kit-bg/src/dbs/local/LocalDbBase.ts b/packages/kit-bg/src/dbs/local/LocalDbBase.ts index 66b4c0246dee..6584fb489258 100644 --- a/packages/kit-bg/src/dbs/local/LocalDbBase.ts +++ b/packages/kit-bg/src/dbs/local/LocalDbBase.ts @@ -142,6 +142,7 @@ import type { IDBWalletNextIdKeys, IDBWalletNextIds, IDBWalletType, + IKeylessWalletDetailsInfo, ILocalDBRecordUpdater, ILocalDBTransaction, ILocalDBTxGetRecordByIdResult, @@ -1019,6 +1020,25 @@ export abstract class LocalDbBase extends LocalDbBaseContainer { } wallet.avatarInfo = avatarInfo; + + let keylessDetailsInfo: IKeylessWalletDetailsInfo | undefined; + if (wallet.keylessDetails) { + try { + const parsedKeylessDetails = JSON.parse( + wallet.keylessDetails || '{}', + ) as IKeylessWalletDetailsInfo; + if ( + parsedKeylessDetails?.keylessOwnerId && + parsedKeylessDetails?.keylessProvider + ) { + keylessDetailsInfo = parsedKeylessDetails; + } + } catch (error) { + console.error('refillWalletInfo keylessDetails', error); + } + } + + wallet.keylessDetailsInfo = keylessDetailsInfo; wallet.walletOrder = wallet.walletOrderSaved ?? wallet.walletNo; if (accountUtils.isHwHiddenWallet({ wallet })) { const parentWallet = await this.getParentWalletOfHiddenWallet({ @@ -1985,6 +2005,7 @@ export abstract class LocalDbBase extends LocalDbBaseContainer { walletHash, walletXfp, isKeylessWallet, + keylessDetailsInfo, } = params; const context = await this.getContext({ verifyPassword: password }); const walletId = accountUtils.buildHdWalletId({ @@ -2018,6 +2039,9 @@ export abstract class LocalDbBase extends LocalDbBaseContainer { walletNo: context.nextWalletNo, deprecated: false, isKeyless: !!isKeylessWallet, + keylessDetails: keylessDetailsInfo + ? JSON.stringify(keylessDetailsInfo) + : undefined, }; currentWalletToCreate = _walletToCreate; currentAvatarInfo = options.avatar; diff --git a/packages/kit-bg/src/dbs/local/types.ts b/packages/kit-bg/src/dbs/local/types.ts index b669f7e135fe..3cb22c15a888 100644 --- a/packages/kit-bg/src/dbs/local/types.ts +++ b/packages/kit-bg/src/dbs/local/types.ts @@ -2,6 +2,7 @@ import type { IBip39RevealableSeed, IBip39RevealableSeedEncryptHex, } from '@onekeyhq/core/src/secret'; +import type { EOAuthSocialLoginProvider } from '@onekeyhq/shared/src/consts/authConsts'; import type { WALLET_TYPE_EXTERNAL, WALLET_TYPE_HD, @@ -133,6 +134,10 @@ export type IDBWalletNextIdKeys = | 'accountGlobalNum' | 'hiddenWalletNum'; export type IDBWalletNextIds = Partial>; +export type IKeylessWalletDetailsInfo = { + keylessOwnerId: string; + keylessProvider: EOAuthSocialLoginProvider; +}; export type IDBWallet = IDBBaseObjectWithName & { type: IDBWalletType; backuped: boolean; @@ -156,6 +161,8 @@ export type IDBWallet = IDBBaseObjectWithName & { isTemp?: boolean; isMocked?: boolean; isKeyless?: boolean; + keylessDetails?: string; // JSON.stringify(keylessDetailsInfo) + keylessDetailsInfo?: IKeylessWalletDetailsInfo; // readonly field passphraseState?: string; walletNo: number; walletOrderSaved?: number; // db field @@ -177,6 +184,7 @@ export type IDBCreateHDWalletParams = { walletXfp: string; avatar?: IAvatarInfo; isKeylessWallet?: boolean; + keylessDetailsInfo?: IKeylessWalletDetailsInfo; }; export type IDBCreateKeylessWalletParams = { password: string; diff --git a/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts b/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts index 0dd40516ed47..4bb02cd91b8f 100644 --- a/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts +++ b/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts @@ -354,6 +354,19 @@ class ServiceAccount extends ServiceBase { return wallets.some((wallet) => wallet.isKeyless); } + @backgroundMethod() + async getKeylessWallet(): Promise { + await timerUtils.wait(1500, { devOnly: true }); + const { wallets } = await localDb.getAllWallets(); + const wallet = wallets.find((w) => w.isKeyless); + if (wallet) { + await localDb.refillWalletInfo({ + wallet, + }); + } + return wallet; + } + async getAllWallets( params: { refillWalletInfo?: boolean; excludeKeylessWallet?: boolean } = {}, ) { @@ -3028,12 +3041,14 @@ class ServiceAccount extends ServiceBase { isWalletBackedUp, isKeylessWallet, avatarInfo, + keylessDetailsInfo, }: { mnemonic: string; name?: string; isWalletBackedUp?: boolean; isKeylessWallet?: boolean; avatarInfo?: IAvatarInfo; + keylessDetailsInfo?: import('../../dbs/local/types').IKeylessWalletDetailsInfo; }) { const { servicePassword } = this.backgroundApi; const { password } = await servicePassword.promptPasswordVerify({ @@ -3075,6 +3090,7 @@ class ServiceAccount extends ServiceBase { isWalletBackedUp, isKeylessWallet, avatarInfo, + keylessDetailsInfo, }); } @@ -3121,6 +3137,7 @@ class ServiceAccount extends ServiceBase { walletXfp, isWalletBackedUp, isKeylessWallet, + keylessDetailsInfo, }: { rs: string; password: string; @@ -3130,6 +3147,7 @@ class ServiceAccount extends ServiceBase { walletXfp: string; isWalletBackedUp?: boolean; isKeylessWallet?: boolean; + keylessDetailsInfo?: import('../../dbs/local/types').IKeylessWalletDetailsInfo; }): Promise<{ wallet: IDBWallet; indexedAccount?: IDBIndexedAccount; @@ -3186,6 +3204,7 @@ class ServiceAccount extends ServiceBase { walletHash, walletXfp, isKeylessWallet, + keylessDetailsInfo, }); await timerUtils.wait(100); diff --git a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts index 1af48100e495..82cd4a8696a6 100644 --- a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts +++ b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts @@ -15,6 +15,11 @@ import { } from '@onekeyhq/shared/src/background/backgroundDecorators'; import type { ICloudBackupKeylessWalletPayload } from '@onekeyhq/shared/src/cloudBackup/cloudBackupTypes'; import { ECloudBackupProviderType } from '@onekeyhq/shared/src/cloudBackup/cloudBackupTypes'; +import { + EOAuthSocialLoginProvider, + SUPABASE_PROJECT_URL, + SUPABASE_PUBLIC_API_KEY, +} from '@onekeyhq/shared/src/consts/authConsts'; import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; import type { IOneKeyError } from '@onekeyhq/shared/src/errors/types/errorTypes'; import type { @@ -34,6 +39,8 @@ import shamirUtils from '@onekeyhq/shared/src/keylessWallet/shamirUtils'; import { appLocale } from '@onekeyhq/shared/src/locale/appLocale'; import { ETranslations } from '@onekeyhq/shared/src/locale/enum/translations'; import appStorage from '@onekeyhq/shared/src/storage/appStorage'; +import supabaseStorageInstance from '@onekeyhq/shared/src/storage/instance/supabaseStorageInstance'; +import { getSupabaseAuthSessionKey } from '@onekeyhq/shared/src/storage/SupabaseStorage/consts'; import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; import bufferUtils from '@onekeyhq/shared/src/utils/bufferUtils'; import type { IAvatarInfo } from '@onekeyhq/shared/src/utils/emojiUtils'; @@ -53,6 +60,7 @@ import ServiceBase from '../ServiceBase'; import keylessAuthPackCache from './utils/keylessAuthPackCache'; import keylessDeviceKeyStorage from './utils/keylessDeviceKeyStorage'; import keylessMnemonicPasswordStorage from './utils/keylessMnemonicPasswordStorage'; +import keylessRefreshTokenStorage from './utils/keylessRefreshTokenStorage'; import type { IDBIndexedAccount, IDBWallet } from '../../dbs/local/types'; import type { IKeylessDialogAtomData } from '../../states/jotai/atoms'; @@ -1156,11 +1164,44 @@ class ServiceKeylessWallet extends ServiceBase { buildKeylessOwnerIdFromSocialToken(params: { token: string }): string { const { token } = params; const decodedToken = stringUtils.decodeJWT(token) as ISupabaseJWTPayload; - const provider = decodedToken?.app_metadata?.provider || ''; + const provider = this.buildKeylessProviderFromSocialToken({ token }); const socialAccountId = decodedToken?.user_metadata?.sub || ''; return `${provider}:${socialAccountId}`; } + buildKeylessProviderFromSocialToken(params: { + token: string; + }): EOAuthSocialLoginProvider { + const { token } = params; + const decodedToken = stringUtils.decodeJWT(token) as ISupabaseJWTPayload; + + // "app_metadata": { + // "provider": "google", + // "provider": "apple", + const provider = decodedToken?.app_metadata?.provider || ''; + if (provider === 'google') { + return EOAuthSocialLoginProvider.Google; + } + if (provider === 'apple') { + return EOAuthSocialLoginProvider.Apple; + } + + // "user_metadata": { + // "iss": "https://accounts.google.com", + // "iss": "https://appleid.apple.com", + const issuer = decodedToken?.user_metadata?.iss || ''; + if (issuer === 'https://accounts.google.com') { + return EOAuthSocialLoginProvider.Google; + } + if (issuer === 'https://appleid.apple.com') { + return EOAuthSocialLoginProvider.Apple; + } + + throw new OneKeyLocalError( + `Unsupported OAuth provider: ${provider}, ${issuer}`, + ); + } + private async apiGetKeylessBackendShare(params: { token: string; }): Promise { @@ -1195,9 +1236,13 @@ class ServiceKeylessWallet extends ServiceBase { // apiVerifyKeylessJuiceboxPin @backgroundMethod() @toastIfError() - async apiVerifyKeylessJuiceboxPin(params: { token: string; pin: string }) { + async apiVerifyKeylessJuiceboxPin(params: { + token: string; + pin: string; + refreshToken?: string; + }) { await timerUtils.wait(1500, { devOnly: true }); - const { token, pin } = params; + const { token, pin, refreshToken } = params; const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); // TODO: Replace with real API call // For now, verify PIN from mock cache @@ -1209,6 +1254,15 @@ class ServiceKeylessWallet extends ServiceBase { if (juiceboxShare?.pin !== pin) { throw new OneKeyLocalError('Invalid PIN'); } + + // Save refresh token to secure storage + if (refreshToken) { + await keylessRefreshTokenStorage.saveRefreshTokenToStorage({ + ownerId, + refreshToken, + backgroundApi: this.backgroundApi, + }); + } } @backgroundMethod() @@ -1288,9 +1342,10 @@ class ServiceKeylessWallet extends ServiceBase { @toastIfError() async resetKeylessWalletPin(params: { token: string | undefined; + refreshToken?: string | undefined; newPin: string | undefined; }) { - const { token, newPin } = params; + const { token, refreshToken, newPin } = params; if (!token) { throw new OneKeyLocalError('social login token is required'); } @@ -1340,6 +1395,15 @@ class ServiceKeylessWallet extends ServiceBase { backendShareX, }); + // Save refresh token to secure storage + if (refreshToken) { + await keylessRefreshTokenStorage.saveRefreshTokenToStorage({ + ownerId, + refreshToken, + backgroundApi: this.backgroundApi, + }); + } + return { success: true }; } @@ -1347,9 +1411,10 @@ class ServiceKeylessWallet extends ServiceBase { @toastIfError() async restoreKeylessWalletFromServer(params: { token: string | undefined; + refreshToken?: string | undefined; pin: string | undefined; }) { - const { token, pin } = params; + const { token, refreshToken, pin } = params; if (!token) { throw new OneKeyLocalError('social login token is required'); } @@ -1407,10 +1472,26 @@ class ServiceKeylessWallet extends ServiceBase { backgroundApi: this.backgroundApi, }); + // Save refresh token to secure storage + if (refreshToken) { + await keylessRefreshTokenStorage.saveRefreshTokenToStorage({ + ownerId, + refreshToken, + backgroundApi: this.backgroundApi, + }); + } + + const keylessProvider = this.buildKeylessProviderFromSocialToken({ token }); + return { + ownerId, mnemonic: await this.backgroundApi.servicePassword.encodeSensitiveText({ text: mnemonic, }), + keylessDetailsInfo: { + keylessOwnerId: ownerId, + keylessProvider, + }, }; } @@ -1418,10 +1499,11 @@ class ServiceKeylessWallet extends ServiceBase { @toastIfError() async createKeylessWalletToServer(params: { token: string | undefined; + refreshToken?: string | undefined; pin: string | undefined; customMnemonic?: string; }) { - const { token, pin, customMnemonic } = params; + const { token, refreshToken, pin, customMnemonic } = params; if (!token) { throw new OneKeyLocalError('social login token is required'); } @@ -1495,10 +1577,26 @@ class ServiceKeylessWallet extends ServiceBase { }); // TODO verify juiceboxShareData is valid + // Save refresh token to secure storage + if (refreshToken) { + await keylessRefreshTokenStorage.saveRefreshTokenToStorage({ + ownerId, + refreshToken, + backgroundApi: this.backgroundApi, + }); + } + + const keylessProvider = this.buildKeylessProviderFromSocialToken({ token }); + return { + ownerId, mnemonic: await this.backgroundApi.servicePassword.encodeSensitiveText({ text: mnemonic, }), + keylessDetailsInfo: { + keylessOwnerId: ownerId, + keylessProvider, + }, }; // TODO cleanup if error occurs } @@ -1515,6 +1613,71 @@ class ServiceKeylessWallet extends ServiceBase { const isCreated = !!backendShareInfo; return isCreated; } + + /** + * Try to refresh access token using stored refreshToken. + * Returns new accessToken and refreshToken if refresh is successful, null otherwise. + */ + @backgroundMethod() + @toastIfError() + async tryRefreshTokenFromStorage(params: { ownerId: string }): Promise<{ + accessToken: string; + refreshToken: string; + } | null> { + const { ownerId } = params; + if (!ownerId) { + throw new OneKeyLocalError('ownerId is required'); + } + try { + // 3. Get refreshToken from secure storage + const storedRefreshToken = + await keylessRefreshTokenStorage.getRefreshTokenFromStorage({ + ownerId, + backgroundApi: this.backgroundApi, + }); + + if (!storedRefreshToken) { + return null; + } + + // 4. Call Supabase HTTP API to refresh token + const refreshUrl = `${SUPABASE_PROJECT_URL}/auth/v1/token?grant_type=refresh_token`; + const response = await fetch(refreshUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // eslint-disable-next-line spellcheck/spell-checker + apikey: SUPABASE_PUBLIC_API_KEY, + Authorization: `Bearer ${SUPABASE_PUBLIC_API_KEY}`, + }, + body: JSON.stringify({ + refresh_token: storedRefreshToken, + }), + }); + + if (!response.ok) { + return null; + } + + const refreshResult = (await response.json()) as { + access_token?: string; + refresh_token?: string; + }; + + if (refreshResult?.access_token && refreshResult?.refresh_token) { + return { + accessToken: refreshResult.access_token, + refreshToken: refreshResult.refresh_token, + }; + } + + return null; + } catch (error) { + // Silently fail - return null if any error occurs + console.error('Failed to refresh token from storage:', error); + return null; + } + } } export default ServiceKeylessWallet; diff --git a/packages/kit-bg/src/services/ServiceKeylessWallet/utils/keylessRefreshTokenStorage.ts b/packages/kit-bg/src/services/ServiceKeylessWallet/utils/keylessRefreshTokenStorage.ts new file mode 100644 index 000000000000..66c96870c724 --- /dev/null +++ b/packages/kit-bg/src/services/ServiceKeylessWallet/utils/keylessRefreshTokenStorage.ts @@ -0,0 +1,137 @@ +import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; +import appStorage from '@onekeyhq/shared/src/storage/appStorage'; +import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; +import bufferUtils from '@onekeyhq/shared/src/utils/bufferUtils'; + +import { buildKeylessLocalEncryptionKey } from './keylessLocalEncryptionKey'; + +import type { IBackgroundApi } from '../../../apis/IBackgroundApi'; + +async function storageSetItem(key: string, encryptedPayloadBase64: string) { + const isSecureStorageSupported = + await appStorage.secureStorage.supportSecureStorage(); + if (isSecureStorageSupported) { + await appStorage.secureStorage.setSecureItem(key, encryptedPayloadBase64); + } else { + await appStorage.setItem(key, encryptedPayloadBase64); + } +} + +async function storageGetItem(key: string): Promise { + const isSecureStorageSupported = + await appStorage.secureStorage.supportSecureStorage(); + if (isSecureStorageSupported) { + return appStorage.secureStorage.getSecureItem(key); + } + return appStorage.getItem(key); +} + +async function storageRemoveItem(key: string): Promise { + const isSecureStorageSupported = + await appStorage.secureStorage.supportSecureStorage(); + if (isSecureStorageSupported) { + await appStorage.secureStorage.removeSecureItem(key); + } else { + await appStorage.removeItem(key); + } +} + +/** + * Save refreshToken to local storage with passcode encryption. + * The refreshToken is encrypted using the user's passcode via buildKeylessLocalEncryptionKey. + * This is used for refreshing access tokens. + */ +async function saveRefreshTokenToStorage(params: { + ownerId: string; + refreshToken: string; + backgroundApi: IBackgroundApi; +}): Promise { + const { ownerId, refreshToken, backgroundApi } = params; + + // 1. Build unique key for this ownerId + const key = accountUtils.buildKeylessRefreshTokenKey({ ownerId }); + + // 2. Encrypt with passcode-based encryption key + // buildKeylessLocalEncryptionKey will prompt for passcode and combine it with sensitiveEncodeKey + const encryptionKey = await buildKeylessLocalEncryptionKey({ backgroundApi }); + + const encryptedPayloadHex = await backgroundApi.servicePassword.encryptString( + { + password: encryptionKey, + data: refreshToken, + dataEncoding: 'utf8', + allowRawPassword: true, + }, + ); + + // Convert hex to base64 for storage + const encryptedPayloadBase64 = bufferUtils.bytesToBase64( + bufferUtils.hexToBytes(encryptedPayloadHex), + ); + + // 3. Store encrypted data, prefer secureStorage if available + await storageSetItem(key, encryptedPayloadBase64); +} + +/** + * Get refreshToken from local storage and decrypt it. + * Requires user passcode to decrypt the refreshToken. + */ +async function getRefreshTokenFromStorage(params: { + ownerId: string; + backgroundApi: IBackgroundApi; +}): Promise { + const { ownerId, backgroundApi } = params; + + // 1. Build unique key for this ownerId + const key = accountUtils.buildKeylessRefreshTokenKey({ ownerId }); + + // 2. Read encrypted data from storage + const encryptedPayloadBase64 = await storageGetItem(key); + + if (!encryptedPayloadBase64) { + return null; + } + + // 3. Decrypt with passcode-based encryption key + // buildKeylessLocalEncryptionKey will prompt for passcode to decrypt + const decryptionKey = await buildKeylessLocalEncryptionKey({ backgroundApi }); + + try { + const refreshToken = await backgroundApi.servicePassword.decryptString({ + password: decryptionKey, + data: encryptedPayloadBase64, + dataEncoding: 'base64', + resultEncoding: 'utf8', + allowRawPassword: true, + }); + return refreshToken; + } catch (error) { + throw new OneKeyLocalError( + `Failed to decrypt refreshToken: invalid password or corrupted data: ${ + (error as Error)?.message + }`, + ); + } +} + +/** + * Remove refreshToken from local storage. + */ +async function removeRefreshTokenFromStorage(params: { + ownerId: string; +}): Promise { + const { ownerId } = params; + + // 1. Build unique key for this ownerId + const key = accountUtils.buildKeylessRefreshTokenKey({ ownerId }); + + // 2. Remove encrypted data from storage + await storageRemoveItem(key); +} + +export default { + saveRefreshTokenToStorage, + getRefreshTokenFromStorage, + removeRefreshTokenFromStorage, +}; diff --git a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx index a8d6ce1d176f..850ba3d14083 100644 --- a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx +++ b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx @@ -324,8 +324,17 @@ async function keylessOnboardingCacheSet(key: string, value: string) { ); } -async function cacheKeylessOnboardingToken({ token }: { token: string }) { +async function cacheKeylessOnboardingToken({ + token, + refreshToken, +}: { + token: string; + refreshToken?: string; +}) { await keylessOnboardingCacheSet('socialLoginToken', token); + if (refreshToken) { + await keylessOnboardingCacheSet('socialLoginRefreshToken', refreshToken); + } } async function getKeylessOnboardingToken(options?: { skipDelete?: boolean }) { @@ -333,6 +342,16 @@ async function getKeylessOnboardingToken(options?: { skipDelete?: boolean }) { return token; } +async function getKeylessOnboardingRefreshToken(options?: { + skipDelete?: boolean; +}) { + const refreshToken = keylessOnboardingCacheGetAndDelete( + 'socialLoginRefreshToken', + options, + ); + return refreshToken; +} + async function cacheKeylessOnboardingPin({ pin }: { pin: string }) { await keylessOnboardingCacheSet('onboardingPin', pin); } @@ -523,13 +542,124 @@ export function useKeylessWallet() { onConfirmText: intl.formatMessage({ id: ETranslations.global_got_it, }), + onCancel: () => { + keylessOnboardingCache.clear(); + }, + onClose: () => { + keylessOnboardingCache.clear(); + }, + onConfirm: () => { + keylessOnboardingCache.clear(); + }, }); throw new OneKeyLocalError('Keyless Wallet onboarding timed out'); }, [intl]); + const checkKeylessWalletCreatedOnServer = useCallback( + async ({ + token, + refreshToken, + mode, + }: { + token: string; + refreshToken?: string; + mode?: EOnboardingV2OneKeyIDLoginMode; + }) => { + if (!token) { + handleKeylessOnboardingTimeout(); + return; + } + await cacheKeylessOnboardingToken({ token, refreshToken }); + + // ResetPin: skip check, go to CreatePin + if (mode === EOnboardingV2OneKeyIDLoginMode.KeylessResetPin) { + navigation.navigate(ERootRoutes.Onboarding, { + screen: EOnboardingV2Routes.OnboardingV2, + params: { + screen: EOnboardingPagesV2.CreatePin, + params: { + action: EKeylessFinalizeAction.ResetPin, + }, + }, + }); + return; + } + + // VerifyPinOnly: skip check, go to VerifyPin + if (mode === EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly) { + navigation.navigate(ERootRoutes.Onboarding, { + screen: EOnboardingV2Routes.OnboardingV2, + params: { + screen: EOnboardingPagesV2.VerifyPin, + params: { + mode: EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly, + }, + }, + }); + return; + } + + // Default: check wallet existence and navigate accordingly + const isCreated = + await backgroundApiProxy.serviceKeylessWallet.isKeylessWalletCreatedOnServer( + { + token, + }, + ); + if (isCreated) { + navigation.navigate(ERootRoutes.Onboarding, { + screen: EOnboardingV2Routes.OnboardingV2, + params: { + screen: EOnboardingPagesV2.VerifyPin, + }, + }); + } else { + navigation.navigate(ERootRoutes.Onboarding, { + screen: EOnboardingV2Routes.OnboardingV2, + params: { + screen: EOnboardingPagesV2.CreatePin, + }, + }); + } + }, + [handleKeylessOnboardingTimeout, navigation], + ); + // goToOneKeyIDLoginPageForKeylessWallet const goToOneKeyIDLoginPageForKeylessWallet = useCallback( - ({ mode }: { mode: EOnboardingV2OneKeyIDLoginMode }) => { + async ({ mode }: { mode: EOnboardingV2OneKeyIDLoginMode }) => { + if ( + mode === EOnboardingV2OneKeyIDLoginMode.KeylessResetPin || + mode === EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly + ) { + // Get keyless wallet to extract ownerId from keylessDetailsInfo + const keylessWallet = + await backgroundApiProxy.serviceAccount.getKeylessWallet(); + const ownerId = keylessWallet?.keylessDetailsInfo?.keylessOwnerId || ''; + + if (keylessWallet && ownerId) { + // Try to refresh session if refreshToken is valid + const refreshResult = + await backgroundApiProxy.serviceKeylessWallet.tryRefreshTokenFromStorage( + { ownerId }, + ); + + if ( + refreshResult && + refreshResult.accessToken && + refreshResult.refreshToken + ) { + // Refresh successful, proceed with checkKeylessWalletCreatedOnServer + await checkKeylessWalletCreatedOnServer({ + token: refreshResult.accessToken, + refreshToken: refreshResult.refreshToken, + mode, + }); + return; + } + } + } + navigation.navigate(ERootRoutes.Onboarding, { screen: EOnboardingV2Routes.OnboardingV2, params: { @@ -540,7 +670,7 @@ export function useKeylessWallet() { }, }); }, - [navigation], + [navigation, checkKeylessWalletCreatedOnServer], ); // Renamed function, checks if KeylessWallet exists locally @@ -567,7 +697,7 @@ export function useKeylessWallet() { }), }); } else { - goToOneKeyIDLoginPageForKeylessWallet({ + await goToOneKeyIDLoginPageForKeylessWallet({ mode: EOnboardingV2OneKeyIDLoginMode.KeylessCreateOrRestore, }); } @@ -577,52 +707,6 @@ export function useKeylessWallet() { }); }, [goToOneKeyIDLoginPageForKeylessWallet, intl]); - const checkKeylessWalletCreatedOnServer = useCallback( - async ({ - token, - mode, - }: { - token: string; - mode?: EOnboardingV2OneKeyIDLoginMode; - }) => { - if (!token) { - handleKeylessOnboardingTimeout(); - return; - } - await cacheKeylessOnboardingToken({ token }); - - // ResetPin: skip check, go to CreatePin - if (mode === EOnboardingV2OneKeyIDLoginMode.KeylessResetPin) { - navigation.push(EOnboardingPagesV2.CreatePin, { - action: EKeylessFinalizeAction.ResetPin, - }); - return; - } - - // VerifyPinOnly: skip check, go to VerifyPin - if (mode === EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly) { - navigation.push(EOnboardingPagesV2.VerifyPin, { - mode: EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly, - }); - return; - } - - // Default: check wallet existence and navigate accordingly - const isCreated = - await backgroundApiProxy.serviceKeylessWallet.isKeylessWalletCreatedOnServer( - { - token, - }, - ); - if (isCreated) { - navigation.push(EOnboardingPagesV2.VerifyPin); - } else { - navigation.push(EOnboardingPagesV2.CreatePin); - } - }, - [handleKeylessOnboardingTimeout, navigation], - ); - const finalizeKeylessWalletV2 = useCallback( async ({ action }: { action: EKeylessFinalizeAction }) => { const token = await getKeylessOnboardingToken(); @@ -630,6 +714,7 @@ export function useKeylessWallet() { handleKeylessOnboardingTimeout(); return; } + const refreshToken = await getKeylessOnboardingRefreshToken(); const pin = await getKeylessOnboardingPin(); if (!pin) { handleKeylessOnboardingTimeout(); @@ -651,37 +736,60 @@ export function useKeylessWallet() { if (action === EKeylessFinalizeAction.ResetPin) { await backgroundApiProxy.serviceKeylessWallet.resetKeylessWalletPin({ token, + refreshToken, newPin: pin, }); - navigation.push(EOnboardingPagesV2.NewPinCreated); + navigation.navigate(ERootRoutes.Onboarding, { + screen: EOnboardingV2Routes.OnboardingV2, + params: { + screen: EOnboardingPagesV2.NewPinCreated, + }, + }); return; } let mnemonic = ''; + let ownerId = ''; + let keylessDetailsInfo; if (action === EKeylessFinalizeAction.Create) { - const customMnemonic = await getKeylessOnboardingCustomMnemonic(); - ({ mnemonic } = + const result = await backgroundApiProxy.serviceKeylessWallet.createKeylessWalletToServer( { token, + refreshToken, pin, - customMnemonic, + customMnemonic: await getKeylessOnboardingCustomMnemonic(), }, - )); + ); + mnemonic = result.mnemonic; + ownerId = result.ownerId; + keylessDetailsInfo = result.keylessDetailsInfo; } if (action === EKeylessFinalizeAction.Restore) { - ({ mnemonic } = + const result = await backgroundApiProxy.serviceKeylessWallet.restoreKeylessWalletFromServer( { token, + refreshToken, pin, }, - )); + ); + mnemonic = result.mnemonic; + ownerId = result.ownerId; + keylessDetailsInfo = result.keylessDetailsInfo; } - navigation.push(EOnboardingPagesV2.FinalizeWalletSetup, { - mnemonic, - isWalletBackedUp: true, - isKeylessWallet: true, + navigation.navigate(ERootRoutes.Onboarding, { + screen: EOnboardingV2Routes.OnboardingV2, + params: { + screen: EOnboardingPagesV2.FinalizeWalletSetup, + params: { + mnemonic, + isWalletBackedUp: true, + isKeylessWallet: true, + keylessOwnerId: ownerId, + keylessDetailsInfo, + }, + }, }); }, [navigation, handleKeylessOnboardingTimeout, intl], @@ -701,7 +809,13 @@ export function useKeylessWallet() { if (hasCachedPassword) { await finalizeKeylessWalletV2({ action }); } else { - navigation.push(EOnboardingPagesV2.CreatePasscode, { action }); + navigation.navigate(ERootRoutes.Onboarding, { + screen: EOnboardingV2Routes.OnboardingV2, + params: { + screen: EOnboardingPagesV2.CreatePasscode, + params: { action }, + }, + }); } }, [finalizeKeylessWalletV2, navigation], @@ -720,10 +834,14 @@ export function useKeylessWallet() { handleKeylessOnboardingTimeout(); return; } + const refreshToken = await getKeylessOnboardingRefreshToken({ + skipDelete: true, + }); await backgroundApiProxy.serviceKeylessWallet.apiVerifyKeylessJuiceboxPin( { token, pin, + refreshToken, }, ); @@ -746,7 +864,7 @@ export function useKeylessWallet() { } // Default: continue with restore flow - await cacheKeylessOnboardingToken({ token }); + await cacheKeylessOnboardingToken({ token, refreshToken }); await confirmKeylessOnboardingPin({ pin, action: EKeylessFinalizeAction.Restore, diff --git a/packages/kit/src/states/jotai/contexts/accountSelector/actions.tsx b/packages/kit/src/states/jotai/contexts/accountSelector/actions.tsx index c1c5b963e82e..67eb1ace622c 100644 --- a/packages/kit/src/states/jotai/contexts/accountSelector/actions.tsx +++ b/packages/kit/src/states/jotai/contexts/accountSelector/actions.tsx @@ -845,10 +845,12 @@ class AccountSelectorActions extends ContextJotaiActionsBase { mnemonic, isWalletBackedUp, isKeylessWallet, + keylessDetailsInfo, }: { mnemonic: string; isWalletBackedUp?: boolean; isKeylessWallet?: boolean; + keylessDetailsInfo?: import('@onekeyhq/kit-bg/src/dbs/local/types').IKeylessWalletDetailsInfo; }, ) => this.withFinalizeWalletSetupStep.call(set, { @@ -858,6 +860,7 @@ class AccountSelectorActions extends ContextJotaiActionsBase { mnemonic, isWalletBackedUp, isKeylessWallet, + keylessDetailsInfo, }); await this.autoSelectToCreatedWallet.call(set, { wallet, diff --git a/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/WalletEditButton.tsx b/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/WalletEditButton.tsx index ae5e5d724b41..bbca23a9aab3 100644 --- a/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/WalletEditButton.tsx +++ b/packages/kit/src/views/AccountManagerStacks/components/WalletEdit/WalletEditButton.tsx @@ -126,7 +126,7 @@ function WalletEditButtonView({ label={intl.formatMessage({ id: ETranslations.reset_pin })} onClose={handleActionListClose} onPress={() => { - goToOneKeyIDLoginPageForKeylessWallet({ + void goToOneKeyIDLoginPageForKeylessWallet({ mode: EOnboardingV2OneKeyIDLoginMode.KeylessResetPin, }); }} @@ -140,7 +140,7 @@ function WalletEditButtonView({ label="Verify PIN" onClose={handleActionListClose} onPress={() => { - goToOneKeyIDLoginPageForKeylessWallet({ + void goToOneKeyIDLoginPageForKeylessWallet({ mode: EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly, }); }} diff --git a/packages/kit/src/views/Onboardingv2/pages/FinalizeWalletSetup.tsx b/packages/kit/src/views/Onboardingv2/pages/FinalizeWalletSetup.tsx index 15da375f8c7e..7be6c3eca7ae 100644 --- a/packages/kit/src/views/Onboardingv2/pages/FinalizeWalletSetup.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/FinalizeWalletSetup.tsx @@ -178,6 +178,7 @@ function FinalizeWalletSetupPage({ const isFirmwareVerified = route?.params?.isFirmwareVerified; const isWalletBackedUp = route?.params?.isWalletBackedUp; const isKeylessWallet = route?.params?.isKeylessWallet; + const keylessDetailsInfo = route?.params?.keylessDetailsInfo; const initialStep = EFinalizeWalletSetupSteps.CreatingWallet; @@ -306,6 +307,7 @@ function FinalizeWalletSetupPage({ mnemonic, isWalletBackedUp, isKeylessWallet, + keylessDetailsInfo, }); }, }); diff --git a/packages/kit/src/views/Onboardingv2/pages/NewPinCreatedPage.tsx b/packages/kit/src/views/Onboardingv2/pages/NewPinCreatedPage.tsx index c12de7d0fedd..bd50c3dfeacf 100644 --- a/packages/kit/src/views/Onboardingv2/pages/NewPinCreatedPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/NewPinCreatedPage.tsx @@ -29,7 +29,7 @@ function NewPinCreatedPage() { return ( - + diff --git a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx index 3fd4b36bf0a4..dc77795f361e 100644 --- a/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/OneKeyIDLoginPage.tsx @@ -125,13 +125,6 @@ function OneKeyIDLoginPage() { const { logout, signInWithSocialLogin } = useOneKeyAuth(); const { checkKeylessWalletCreatedOnServer } = useKeylessWallet(); - const goToInputPinPage = useCallback( - async ({ token }: { token: string }) => { - await checkKeylessWalletCreatedOnServer({ token, mode }); - }, - [checkKeylessWalletCreatedOnServer, mode], - ); - const handleSocialLogin = useCallback( async (provider: EOAuthSocialLoginProvider) => { if (loggingInProviderRef.current) { @@ -141,15 +134,17 @@ function OneKeyIDLoginPage() { setLoggingInProvider(provider); const result = await signInWithSocialLogin(provider); if (result?.session?.accessToken) { - await goToInputPinPage({ + await checkKeylessWalletCreatedOnServer({ token: result.session.accessToken, + refreshToken: result.session.refreshToken, + mode, }); } } finally { setLoggingInProvider(null); } }, - [goToInputPinPage, signInWithSocialLogin], + [checkKeylessWalletCreatedOnServer, mode, signInWithSocialLogin], ); const handleGoogleLogin = useCallback(async () => { diff --git a/packages/kit/src/views/Setting/pages/Tab/SettingListSubModal.tsx b/packages/kit/src/views/Setting/pages/Tab/SettingListSubModal.tsx index bf1ea92b0aa8..94d276af296c 100644 --- a/packages/kit/src/views/Setting/pages/Tab/SettingListSubModal.tsx +++ b/packages/kit/src/views/Setting/pages/Tab/SettingListSubModal.tsx @@ -2,10 +2,12 @@ import { useMemo } from 'react'; import { useRoute } from '@react-navigation/core'; +import { AccountSelectorProviderMirror } from '@onekeyhq/kit/src/components/AccountSelector'; import type { EModalSettingRoutes, IModalSettingParamList, } from '@onekeyhq/shared/src/routes'; +import { EAccountSelectorSceneName } from '@onekeyhq/shared/types'; import { useSettingsConfig } from './config'; import { SubSettingsPage } from './SubSettingsPage'; @@ -14,7 +16,7 @@ import type { RouteProp } from '@react-navigation/core'; type ISettingName = string; -export default function SettingListSubModal() { +function SettingListSubModalView() { const route = useRoute< RouteProp @@ -32,3 +34,16 @@ export default function SettingListSubModal() { /> ); } + +export default function SettingListSubModal() { + return ( + + + + ); +} diff --git a/packages/kit/src/views/Setting/pages/Tab/config.tsx b/packages/kit/src/views/Setting/pages/Tab/config.tsx index cbabc97f3d5b..a4dc1f9e2a0e 100644 --- a/packages/kit/src/views/Setting/pages/Tab/config.tsx +++ b/packages/kit/src/views/Setting/pages/Tab/config.tsx @@ -525,7 +525,7 @@ export const useSettingsConfig: () => ISettingsConfig = () => { icon: 'InputOutline', title: intl.formatMessage({ id: ETranslations.reset_pin }), onPress: (navigation) => { - goToOneKeyIDLoginPageForKeylessWallet({ + void goToOneKeyIDLoginPageForKeylessWallet({ mode: EOnboardingV2OneKeyIDLoginMode.KeylessResetPin, }); }, diff --git a/packages/shared/src/keylessWallet/keylessWalletTypes.ts b/packages/shared/src/keylessWallet/keylessWalletTypes.ts index 61c7e926b339..427ccbd6036d 100644 --- a/packages/shared/src/keylessWallet/keylessWalletTypes.ts +++ b/packages/shared/src/keylessWallet/keylessWalletTypes.ts @@ -123,5 +123,6 @@ export type ISupabaseJWTPayload = JWTPayload & { }; user_metadata: { sub: string; + iss: string; }; }; diff --git a/packages/shared/src/routes/onboardingv2.ts b/packages/shared/src/routes/onboardingv2.ts index a25463754fda..f0bfe47988ca 100644 --- a/packages/shared/src/routes/onboardingv2.ts +++ b/packages/shared/src/routes/onboardingv2.ts @@ -80,6 +80,8 @@ export type IOnboardingParamListV2 = { isFirmwareVerified?: boolean; deviceData?: IConnectYourDeviceItem; keylessPackSetId?: string; + keylessOwnerId?: string; + keylessDetailsInfo?: import('../../kit-bg/src/dbs/local/types').IKeylessWalletDetailsInfo; }; [EOnboardingPagesV2.PickYourDevice]: undefined; [EOnboardingPagesV2.ConnectYourDevice]: { diff --git a/packages/shared/src/utils/accountUtils.ts b/packages/shared/src/utils/accountUtils.ts index f1975ad84364..fc4aa344e4c2 100644 --- a/packages/shared/src/utils/accountUtils.ts +++ b/packages/shared/src/utils/accountUtils.ts @@ -1023,6 +1023,14 @@ function buildKeylessMnemonicPasswordKey({ return `OneKey_Keyless_MnemonicPwd__${ownerId}`; } +function buildKeylessRefreshTokenKey({ + ownerId, +}: { + ownerId: string; +}): string { + return `OneKey_Keyless_RefreshToken__${ownerId}`; +} + export default { URL_ACCOUNT_ID, HYPERLIQUID_AGENT_CREDENTIAL_PREFIX, @@ -1030,6 +1038,7 @@ export default { getKeylessWalletPackSetId, buildKeylessDevicePackKey, buildKeylessMnemonicPasswordKey, + buildKeylessRefreshTokenKey, buildKeylessWalletId, buildAccountValueKey, parseAccountValueKey, From f0b38701cdd550c8aa83432aebb0b3d09e4eebaf Mon Sep 17 00:00:00 2001 From: morizon Date: Tue, 30 Dec 2025 18:06:56 +0800 Subject: [PATCH 44/66] feat: add error handling for keyless wallet retrieval and refresh token processes; improve navigation logic in onboarding flow --- .../KeylessWallet/useKeylessWallet.tsx | 28 +++++++++++++------ .../pages/FinalizeWalletSetup.tsx | 3 +- packages/shared/src/routes/onboardingv2.ts | 4 ++- packages/shared/src/utils/accountUtils.ts | 6 +--- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx index 850ba3d14083..8994a22474ab 100644 --- a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx +++ b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx @@ -573,6 +573,7 @@ export function useKeylessWallet() { // ResetPin: skip check, go to CreatePin if (mode === EOnboardingV2OneKeyIDLoginMode.KeylessResetPin) { + // TODO check if keyless wallet matched with ownerId navigation.navigate(ERootRoutes.Onboarding, { screen: EOnboardingV2Routes.OnboardingV2, params: { @@ -587,6 +588,7 @@ export function useKeylessWallet() { // VerifyPinOnly: skip check, go to VerifyPin if (mode === EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly) { + // TODO check if keyless wallet matched with ownerId navigation.navigate(ERootRoutes.Onboarding, { screen: EOnboardingV2Routes.OnboardingV2, params: { @@ -633,21 +635,31 @@ export function useKeylessWallet() { mode === EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly ) { // Get keyless wallet to extract ownerId from keylessDetailsInfo - const keylessWallet = - await backgroundApiProxy.serviceAccount.getKeylessWallet(); + let keylessWallet; + try { + keylessWallet = + await backgroundApiProxy.serviceAccount.getKeylessWallet(); + } catch (error) { + // Continue to navigation if getKeylessWallet fails + } const ownerId = keylessWallet?.keylessDetailsInfo?.keylessOwnerId || ''; if (keylessWallet && ownerId) { // Try to refresh session if refreshToken is valid - const refreshResult = - await backgroundApiProxy.serviceKeylessWallet.tryRefreshTokenFromStorage( - { ownerId }, - ); + let refreshResult; + try { + refreshResult = + await backgroundApiProxy.serviceKeylessWallet.tryRefreshTokenFromStorage( + { ownerId }, + ); + } catch (error) { + // Continue to navigation if refresh fails + } if ( refreshResult && - refreshResult.accessToken && - refreshResult.refreshToken + refreshResult?.accessToken && + refreshResult?.refreshToken ) { // Refresh successful, proceed with checkKeylessWalletCreatedOnServer await checkKeylessWalletCreatedOnServer({ diff --git a/packages/kit/src/views/Onboardingv2/pages/FinalizeWalletSetup.tsx b/packages/kit/src/views/Onboardingv2/pages/FinalizeWalletSetup.tsx index 7be6c3eca7ae..c5fbfd60c04f 100644 --- a/packages/kit/src/views/Onboardingv2/pages/FinalizeWalletSetup.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/FinalizeWalletSetup.tsx @@ -345,14 +345,15 @@ function FinalizeWalletSetupPage({ mnemonic, deviceData, isFirmwareVerified, + keylessPackSetId, mnemonicType, actions, isWalletBackedUp, isKeylessWallet, + keylessDetailsInfo, goNextStep, connectDevice, createHWWallet, - keylessPackSetId, ]); useEffect(() => { diff --git a/packages/shared/src/routes/onboardingv2.ts b/packages/shared/src/routes/onboardingv2.ts index f0bfe47988ca..b1d98553e69a 100644 --- a/packages/shared/src/routes/onboardingv2.ts +++ b/packages/shared/src/routes/onboardingv2.ts @@ -1,3 +1,5 @@ +import type { IKeylessWalletDetailsInfo } from '@onekeyhq/kit-bg/src/dbs/local/types'; + import type { EConnectDeviceChannel } from '../../types/connectDevice'; import type { IConnectYourDeviceItem } from '../../types/device'; import type { EKeylessFinalizeAction } from '../keylessWallet/keylessWalletConsts'; @@ -81,7 +83,7 @@ export type IOnboardingParamListV2 = { deviceData?: IConnectYourDeviceItem; keylessPackSetId?: string; keylessOwnerId?: string; - keylessDetailsInfo?: import('../../kit-bg/src/dbs/local/types').IKeylessWalletDetailsInfo; + keylessDetailsInfo?: IKeylessWalletDetailsInfo; }; [EOnboardingPagesV2.PickYourDevice]: undefined; [EOnboardingPagesV2.ConnectYourDevice]: { diff --git a/packages/shared/src/utils/accountUtils.ts b/packages/shared/src/utils/accountUtils.ts index fc4aa344e4c2..ff48cf2fb8e5 100644 --- a/packages/shared/src/utils/accountUtils.ts +++ b/packages/shared/src/utils/accountUtils.ts @@ -1023,11 +1023,7 @@ function buildKeylessMnemonicPasswordKey({ return `OneKey_Keyless_MnemonicPwd__${ownerId}`; } -function buildKeylessRefreshTokenKey({ - ownerId, -}: { - ownerId: string; -}): string { +function buildKeylessRefreshTokenKey({ ownerId }: { ownerId: string }): string { return `OneKey_Keyless_RefreshToken__${ownerId}`; } From 26d3e73381ca11a630aa8918cc1af680d4c2c595 Mon Sep 17 00:00:00 2001 From: morizon Date: Tue, 30 Dec 2025 18:13:32 +0800 Subject: [PATCH 45/66] refactor: remove socialLoginProvider prop from WalletAvatarProps and derive it from wallet keyless details for improved clarity and maintainability --- packages/kit/src/components/WalletAvatar/WalletAvatar.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/kit/src/components/WalletAvatar/WalletAvatar.tsx b/packages/kit/src/components/WalletAvatar/WalletAvatar.tsx index e49fac09107a..3023ee7eb8f1 100644 --- a/packages/kit/src/components/WalletAvatar/WalletAvatar.tsx +++ b/packages/kit/src/components/WalletAvatar/WalletAvatar.tsx @@ -24,7 +24,6 @@ export type IWalletAvatarProps = IWalletAvatarBaseProps & { status?: IWalletProps['status']; badge?: number | string; firmwareTypeBadge?: EFirmwareType; - socialLoginProvider?: EOAuthSocialLoginProvider; }; export function WalletAvatarBase({ @@ -67,10 +66,10 @@ export function WalletAvatar({ status, badge, firmwareTypeBadge, - socialLoginProvider, img, wallet, }: IWalletAvatarProps) { + const socialLoginProvider = wallet?.keylessDetailsInfo?.keylessProvider; return ( From bee7d8bd6ee0a9d0000546fabd3a9b1ed0f1e7cd Mon Sep 17 00:00:00 2001 From: morizon Date: Wed, 31 Dec 2025 09:56:19 +0800 Subject: [PATCH 46/66] refactor: adjust wallet creation flow in ServiceKeylessWallet to ensure juiceboxShare is uploaded before backend share, preventing concurrent wallet creation --- .../ServiceKeylessWallet.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts index 82cd4a8696a6..f9bbe64bedfe 100644 --- a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts +++ b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts @@ -1559,14 +1559,7 @@ class ServiceKeylessWallet extends ServiceBase { }); // TODO verify mnemonicPassword is saved successfully - const _backendShareData: IKeylessBackendShare = - await this.apiUploadKeylessBackendShare({ - token, - encryptedMnemonic, - backendShare, - juiceboxShareX, // Store the other share's x-coordinate for recovery - }); - // TODO verify backendShareData is valid + // TODO lock server creation flow, avoid multiple clients creating new wallets at the same time const _juiceboxShareData: IKeylessJuiceboxShare = await this.apiUploadKeylessJuiceboxShare({ @@ -1577,6 +1570,16 @@ class ServiceKeylessWallet extends ServiceBase { }); // TODO verify juiceboxShareData is valid + // make sure juiceboxShare is uploaded successfully before uploading backend share + const _backendShareData: IKeylessBackendShare = + await this.apiUploadKeylessBackendShare({ + token, + encryptedMnemonic, + backendShare, + juiceboxShareX, // Store the other share's x-coordinate for recovery + }); + // TODO verify backendShareData is valid + // Save refresh token to secure storage if (refreshToken) { await keylessRefreshTokenStorage.saveRefreshTokenToStorage({ From 86ff35394d9cc50097ed49afc92f3839ef80dbed Mon Sep 17 00:00:00 2001 From: morizon Date: Wed, 31 Dec 2025 10:41:01 +0800 Subject: [PATCH 47/66] fix: update Google OAuth client IDs in Info.plist and authConsts.ts for iOS compatibility --- apps/mobile/ios/OneKeyWallet/Info.plist | 2 +- .../hooks/useAutoSelectAccount.tsx | 2 +- .../kit/src/views/Home/pages/HomePageView.tsx | 14 ++++---- .../views/Home/pages/TokenListContainer.tsx | 36 +++++++++---------- packages/shared/src/consts/authConsts.ts | 6 ++-- 5 files changed, 31 insertions(+), 29 deletions(-) diff --git a/apps/mobile/ios/OneKeyWallet/Info.plist b/apps/mobile/ios/OneKeyWallet/Info.plist index 71bcbcf5b65f..4119a3015f0b 100644 --- a/apps/mobile/ios/OneKeyWallet/Info.plist +++ b/apps/mobile/ios/OneKeyWallet/Info.plist @@ -38,7 +38,7 @@ Editor CFBundleURLSchemes - com.googleusercontent.apps.244450898872-1jvugg12bmstu8nfqfmcpf1o7tcsoltt + com.googleusercontent.apps.94391474021-kbgarvu23k3mblp1m2tiknemae99p826 diff --git a/packages/kit/src/components/AccountSelector/hooks/useAutoSelectAccount.tsx b/packages/kit/src/components/AccountSelector/hooks/useAutoSelectAccount.tsx index 54f0aec7f05b..4c1f9b0ceb93 100644 --- a/packages/kit/src/components/AccountSelector/hooks/useAutoSelectAccount.tsx +++ b/packages/kit/src/components/AccountSelector/hooks/useAutoSelectAccount.tsx @@ -10,13 +10,13 @@ import { EAccountSelectorSceneName, } from '@onekeyhq/shared/types'; -import { deferHeavyWorkUntilUIIdle } from '../../../utils/deferHeavyWork'; import { useAccountSelectorActions, useAccountSelectorSceneInfo, useAccountSelectorStorageReadyAtom, useActiveAccount, } from '../../../states/jotai/contexts/accountSelector'; +import { deferHeavyWorkUntilUIIdle } from '../../../utils/deferHeavyWork'; export function useAutoSelectAccount({ num }: { num: number }) { const { diff --git a/packages/kit/src/views/Home/pages/HomePageView.tsx b/packages/kit/src/views/Home/pages/HomePageView.tsx index 2c70bc41101b..aa06abedf626 100644 --- a/packages/kit/src/views/Home/pages/HomePageView.tsx +++ b/packages/kit/src/views/Home/pages/HomePageView.tsx @@ -1,8 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useIntl } from 'react-intl'; - import { CanceledError } from 'axios'; +import { useIntl } from 'react-intl'; import type { ITabContainerRef } from '@onekeyhq/components'; import { @@ -203,17 +202,18 @@ export function HomePageView({ if (cancelled) return; try { - const resp = await backgroundApiProxy.serviceApproval.fetchAccountApprovals( - { + const resp = + await backgroundApiProxy.serviceApproval.fetchAccountApprovals({ networkId: network.id, accountId: account.id, indexedAccountId: indexedAccount?.id, accountAddress: account.address, - }, - ); + }); if (cancelled) return; updateApprovalsInfo({ - hasRiskApprovals: resp.contractApprovals.some((i) => i.isRiskContract), + hasRiskApprovals: resp.contractApprovals.some( + (i) => i.isRiskContract, + ), }); } catch (error) { if (error instanceof CanceledError) { diff --git a/packages/kit/src/views/Home/pages/TokenListContainer.tsx b/packages/kit/src/views/Home/pages/TokenListContainer.tsx index bd2daaa3da77..febf82c458d5 100644 --- a/packages/kit/src/views/Home/pages/TokenListContainer.tsx +++ b/packages/kit/src/views/Home/pages/TokenListContainer.tsx @@ -1932,24 +1932,24 @@ function TokenListContainer({ ]); return ( - Date: Wed, 31 Dec 2025 11:51:25 +0800 Subject: [PATCH 48/66] feat: add display of Google OAuth client IDs in OneKeyIDApiTests for enhanced debugging --- .../Components/stories/OneKeyIDGallery.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx index 11e91a4f497d..fd793b6bc199 100644 --- a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx +++ b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx @@ -20,6 +20,7 @@ import { useOneKeyAuth } from '@onekeyhq/kit/src/components/OneKeyAuth/useOneKey import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; import { EOAuthSocialLoginProvider, + GOOGLE_OAUTH_CLIENT_IDS, SUPABASE_PROJECT_URL, SUPABASE_PUBLIC_API_KEY, } from '@onekeyhq/shared/src/consts/authConsts'; @@ -148,7 +149,7 @@ function OneKeyIDApiTests() { > - PROJECT_URL: + SUPABASE_PROJECT_URL: {SUPABASE_PROJECT_URL || '(empty)'} @@ -156,12 +157,28 @@ function OneKeyIDApiTests() { - PUBLIC_API_KEY: + SUPABASE_PUBLIC_API_KEY: {SUPABASE_PUBLIC_API_KEY || '(empty)'} + + + GOOGLE_OAUTH_CLIENT_WEB: + + + {GOOGLE_OAUTH_CLIENT_IDS.WEB || '(empty)'} + + + + + GOOGLE_OAUTH_CLIENT_IOS: + + + {GOOGLE_OAUTH_CLIENT_IDS.IOS || '(empty)'} + + From c04e86118631ce3401754c8a880490523079d49a Mon Sep 17 00:00:00 2001 From: morizon Date: Wed, 31 Dec 2025 17:35:56 +0800 Subject: [PATCH 49/66] feat: integrate Juicebox for Keyless Wallet discovery and recovery --- package.json | 1 + .../ServiceKeylessWallet.ts | 48 ++-- .../utils/JuiceboxClient.ts | 215 ++++++++++++++++++ .../KeylessWallet/useKeylessWallet.tsx | 5 +- .../OneKeyAuth/supabase/getSupabaseClient.ts | 6 +- .../Components/stories/OneKeyIDGallery.tsx | 106 ++++++++- packages/shared/src/consts/authConsts.ts | 36 +++ .../src/storage/SupabaseStorage/consts.ts | 2 +- yarn.lock | 8 + 9 files changed, 395 insertions(+), 32 deletions(-) create mode 100644 packages/kit-bg/src/services/ServiceKeylessWallet/utils/JuiceboxClient.ts diff --git a/package.json b/package.json index 5e26dcdb8671..b9f34fa52bf1 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "isomorphic-ws": "^4.0.1", "jotai": "^2.5.0", "js-md5": "^0.8.3", + "juicebox-sdk": "^0.3.4", "lightweight-charts": "^3.8.0", "long": "^5.2.1", "lru-cache": "^10.2.0", diff --git a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts index f9bbe64bedfe..698dba27edef 100644 --- a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts +++ b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts @@ -1,9 +1,7 @@ import { isEqual } from 'lodash'; -import { LRUCache } from 'lru-cache'; import { decryptStringAsync, - encryptAsync, encryptStringAsync, generateMnemonic, mnemonicToEntropy, @@ -17,8 +15,8 @@ import type { ICloudBackupKeylessWalletPayload } from '@onekeyhq/shared/src/clou import { ECloudBackupProviderType } from '@onekeyhq/shared/src/cloudBackup/cloudBackupTypes'; import { EOAuthSocialLoginProvider, - SUPABASE_PROJECT_URL, - SUPABASE_PUBLIC_API_KEY, + KEYLESS_SUPABASE_PROJECT_URL, + KEYLESS_SUPABASE_PUBLIC_API_KEY, } from '@onekeyhq/shared/src/consts/authConsts'; import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; import type { IOneKeyError } from '@onekeyhq/shared/src/errors/types/errorTypes'; @@ -39,18 +37,15 @@ import shamirUtils from '@onekeyhq/shared/src/keylessWallet/shamirUtils'; import { appLocale } from '@onekeyhq/shared/src/locale/appLocale'; import { ETranslations } from '@onekeyhq/shared/src/locale/enum/translations'; import appStorage from '@onekeyhq/shared/src/storage/appStorage'; -import supabaseStorageInstance from '@onekeyhq/shared/src/storage/instance/supabaseStorageInstance'; -import { getSupabaseAuthSessionKey } from '@onekeyhq/shared/src/storage/SupabaseStorage/consts'; import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; import bufferUtils from '@onekeyhq/shared/src/utils/bufferUtils'; import type { IAvatarInfo } from '@onekeyhq/shared/src/utils/emojiUtils'; import { findMismatchedPaths } from '@onekeyhq/shared/src/utils/miscUtils'; import stringUtils from '@onekeyhq/shared/src/utils/stringUtils'; import timerUtils from '@onekeyhq/shared/src/utils/timerUtils'; -import { EServiceEndpointEnum } from '@onekeyhq/shared/types/endpoint'; import type { IApiClientResponse } from '@onekeyhq/shared/types/endpoint'; +import { EServiceEndpointEnum } from '@onekeyhq/shared/types/endpoint'; import { EPrimeTransferDataType } from '@onekeyhq/shared/types/prime/primeTransferTypes'; -import { EReasonForNeedPassword } from '@onekeyhq/shared/types/setting'; import localDb from '../../dbs/local/localDb'; import { keylessDialogAtom, primePersistAtom } from '../../states/jotai/atoms'; @@ -1317,15 +1312,31 @@ class ServiceKeylessWallet extends ServiceBase { juiceboxShare, backendShareX, }; - const mockedShares = await this.getMockedKeylessShares(); - if (!mockedShares[ownerId]) { - mockedShares[ownerId] = {} as { - backendShare: IKeylessBackendShare; - juiceboxShare: IKeylessJuiceboxShare; - }; + // const mockedShares = await this.getMockedKeylessShares(); + // if (!mockedShares[ownerId]) { + // mockedShares[ownerId] = {} as { + // backendShare: IKeylessBackendShare; + // juiceboxShare: IKeylessJuiceboxShare; + // }; + // } + // mockedShares[ownerId].juiceboxShare = juiceboxShareData; + // await this.saveMockedKeylessShares(mockedShares); + + const { JuiceboxClient } = await import('./utils/JuiceboxClient'); + const juiceboxClient = new JuiceboxClient(); + await juiceboxClient.exchangeToken(token); + try { + const secret = `${juiceboxShare}--${backendShareX}`; + await juiceboxClient.register({ + pin, + secret, + userInfo: ownerId, + }); + } catch (e) { + console.error(e); + throw e; } - mockedShares[ownerId].juiceboxShare = juiceboxShareData; - await this.saveMockedKeylessShares(mockedShares); + return juiceboxShareData; } @@ -1644,14 +1655,13 @@ class ServiceKeylessWallet extends ServiceBase { } // 4. Call Supabase HTTP API to refresh token - const refreshUrl = `${SUPABASE_PROJECT_URL}/auth/v1/token?grant_type=refresh_token`; + const refreshUrl = `${KEYLESS_SUPABASE_PROJECT_URL}/auth/v1/token?grant_type=refresh_token`; const response = await fetch(refreshUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', // eslint-disable-next-line spellcheck/spell-checker - apikey: SUPABASE_PUBLIC_API_KEY, - Authorization: `Bearer ${SUPABASE_PUBLIC_API_KEY}`, + apikey: KEYLESS_SUPABASE_PUBLIC_API_KEY, }, body: JSON.stringify({ refresh_token: storedRefreshToken, diff --git a/packages/kit-bg/src/services/ServiceKeylessWallet/utils/JuiceboxClient.ts b/packages/kit-bg/src/services/ServiceKeylessWallet/utils/JuiceboxClient.ts new file mode 100644 index 000000000000..93e54e72cbdd --- /dev/null +++ b/packages/kit-bg/src/services/ServiceKeylessWallet/utils/JuiceboxClient.ts @@ -0,0 +1,215 @@ +import axios from 'axios'; +import { Client, Configuration } from 'juicebox-sdk'; + +import { + JUICEBOX_ALLOWED_GUESSES, + JUICEBOX_AUTH_SERVER, + JUICEBOX_CONFIG, +} from '@onekeyhq/shared/src/consts/authConsts'; +import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; +import bufferUtils from '@onekeyhq/shared/src/utils/bufferUtils'; +import type { IApiClientResponse } from '@onekeyhq/shared/types/endpoint'; + +/** + * JuiceboxClient - Wrapper for juicebox-sdk to handle keyless wallet shares + * + * This class provides methods to register and recover shares in the Juicebox network. + * It handles token management, caching, and error handling internally. + */ +export class JuiceboxClient { + // Private properties + + private juiceboxTokenCache: Map; // realmId (hex) -> JWT token + + private client: Client; // Client from juicebox-sdk + + /** + * Constructor + * @param backgroundApi - Background API instance to access OneKeyID tokens + */ + constructor() { + this.juiceboxTokenCache = new Map(); + + // Set global callback for juicebox-sdk + // @ts-ignore + globalThis.JuiceboxGetAuthToken = (realmId: Uint8Array) => + this.getAuthTokenForRealm(realmId); + + // Create internal Client instance from juicebox-sdk + this.client = new Client(new Configuration(JUICEBOX_CONFIG), []); + } + + /** + * Exchange Supabase token for Juicebox tokens for all realms + * This method must be called before register() or recover() + * + * @param supabaseAccessToken - Supabase access token for authentication + * @returns Promise - Resolves when all tokens are cached + * @throws {OneKeyLocalError} - If token exchange fails + */ + async exchangeToken(supabaseAccessToken: string): Promise { + if (!supabaseAccessToken) { + throw new OneKeyLocalError('Supabase access token is required'); + } + + const tokenUrl = `${JUICEBOX_AUTH_SERVER}/juicebox/v1/token/realms`; + + const response = await axios.post< + IApiClientResponse<{ + tokens: Record; + }> + >(tokenUrl, { + token: supabaseAccessToken, + }); + const resData = response?.data; + if (resData?.code === 0 && resData?.data?.tokens) { + const realmTokens = resData?.data?.tokens; + // Validate response format + if (!realmTokens || typeof realmTokens !== 'object') { + throw new OneKeyLocalError( + 'Invalid response format: expected object with realm tokens', + ); + } + + // Cache all realm tokens + for (const [realmId, token] of Object.entries(realmTokens)) { + if (!token) { + throw new OneKeyLocalError( + `Invalid response format: missing token for realm ${realmId}`, + ); + } + this.juiceboxTokenCache.set(realmId, token); + } + + // Verify all configured realms have tokens + for (const realm of JUICEBOX_CONFIG.realms) { + if (!this.juiceboxTokenCache.has(realm.id)) { + throw new OneKeyLocalError( + `Missing token for configured realm: ${realm.id}`, + ); + } + } + } else { + throw new OneKeyLocalError( + `Get Juicebox Token Error: ${resData?.code} ${resData?.message}`, + ); + } + } + + /** + * Register a share with PIN and userInfo in the Juicebox network + * + * @param pin - User PIN (string) + * @param secret - Share data to store (utf8 string, will be decoded to Uint8Array) + * @param userInfo - User identifier, typically ownerId (string) + * @returns Promise - Resolves when registration is successful + * @throws {OneKeyLocalError} - If registration fails or juiceboxTokenCache is empty + * @throws {RegisterError} - SDK-specific registration errors + */ + async register(params: { + pin: string; + secret: string; // utf8 string + userInfo: string; // ownerId + }): Promise { + const { pin, secret, userInfo } = params; + + // Validate token cache is not empty + if (this.juiceboxTokenCache.size === 0) { + throw new OneKeyLocalError( + 'Juicebox token cache is empty, please call exchangeToken first', + ); + } + + // Convert strings to Uint8Array + const pinBytes = bufferUtils.utf8ToBytes(pin); + const secretBytes = bufferUtils.utf8ToBytes(secret); + const userInfoBytes = bufferUtils.utf8ToBytes(userInfo); + + // Call SDK register method + await this.client.register( + pinBytes, + secretBytes, // secret exceeds the maximum of 128 bytes + userInfoBytes, + JUICEBOX_ALLOWED_GUESSES, + ); + + // Clear token cache after successful registration + this.clearTokenCache(); + } + + /** + * Recover a share from the Juicebox network using PIN and userInfo + * + * @param pin - User PIN (string) + * @param userInfo - User identifier, typically ownerId (string) + * @returns Promise - Recovered share data as utf8 string + * @throws {OneKeyLocalError} - If recovery fails or juiceboxTokenCache is empty + * @throws {RecoverError} - SDK-specific recovery errors + */ + async recover(params: { + pin: string; + userInfo: string; // ownerId + }): Promise { + const { pin, userInfo } = params; + + // Validate token cache is not empty + if (this.juiceboxTokenCache.size === 0) { + throw new OneKeyLocalError( + 'Juicebox token cache is empty, please call exchangeToken first', + ); + } + + // Convert strings to Uint8Array + const pinBytes = bufferUtils.utf8ToBytes(pin); + const userInfoBytes = bufferUtils.utf8ToBytes(userInfo); + + // Call SDK recover method + const recoveredSecret: Uint8Array = await this.client.recover( + pinBytes, + userInfoBytes, + ); + + if (!recoveredSecret?.length) { + throw new OneKeyLocalError('Recovery failed: Empty secret.'); + } + + // Convert recovered Uint8Array back to utf8 string + const secretUtf8 = bufferUtils.bytesToUtf8(recoveredSecret); + + // Clear token cache after successful recovery + this.clearTokenCache(); + + return secretUtf8; + } + + /** + * Get authentication token for a specific realm + * This method is called by the global JuiceboxGetAuthToken callback + * + * @param realmId - Realm ID as Uint8Array (from juicebox-sdk) + * @returns Promise - JWT token for the realm + * @throws {OneKeyLocalError} - If token not found in cache + */ + private async getAuthTokenForRealm(realmId: Uint8Array): Promise { + // Convert realmId to hex string + const realmIdHex = bufferUtils.bytesToHex(realmId); + + // Get token from cache + const token = this.juiceboxTokenCache.get(realmIdHex); + if (!token) { + throw new OneKeyLocalError( + 'Juicebox token not found, please call exchangeToken first', + ); + } + + return token; + } + + /** + * Clear all cached tokens + * Useful for logout scenarios or when tokens need to be refreshed + */ + clearTokenCache(): void { + this.juiceboxTokenCache.clear(); + } +} diff --git a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx index 8994a22474ab..8dac88cd71a1 100644 --- a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx +++ b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx @@ -544,16 +544,19 @@ export function useKeylessWallet() { }), onCancel: () => { keylessOnboardingCache.clear(); + navigation.popStack(); }, onClose: () => { keylessOnboardingCache.clear(); + navigation.popStack(); }, onConfirm: () => { keylessOnboardingCache.clear(); + navigation.popStack(); }, }); throw new OneKeyLocalError('Keyless Wallet onboarding timed out'); - }, [intl]); + }, [intl, navigation]); const checkKeylessWalletCreatedOnServer = useCallback( async ({ diff --git a/packages/kit/src/components/OneKeyAuth/supabase/getSupabaseClient.ts b/packages/kit/src/components/OneKeyAuth/supabase/getSupabaseClient.ts index 9b032f97587c..81004f0f3672 100644 --- a/packages/kit/src/components/OneKeyAuth/supabase/getSupabaseClient.ts +++ b/packages/kit/src/components/OneKeyAuth/supabase/getSupabaseClient.ts @@ -2,6 +2,8 @@ import { createClient } from '@supabase/supabase-js'; import { + KEYLESS_SUPABASE_PROJECT_URL, + KEYLESS_SUPABASE_PUBLIC_API_KEY, SUPABASE_PROJECT_URL, SUPABASE_PUBLIC_API_KEY, } from '@onekeyhq/shared/src/consts/authConsts'; @@ -43,8 +45,8 @@ export function getSupabaseClient() { */ export function createTemporarySupabaseClient() { return createClient( - SUPABASE_PROJECT_URL ?? '', - SUPABASE_PUBLIC_API_KEY ?? '', + KEYLESS_SUPABASE_PROJECT_URL ?? '', + KEYLESS_SUPABASE_PUBLIC_API_KEY ?? '', { auth: { autoRefreshToken: false, diff --git a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx index fd793b6bc199..c5e203252e89 100644 --- a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx +++ b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/OneKeyIDGallery.tsx @@ -18,9 +18,14 @@ import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/background import { useSupabaseAuth } from '@onekeyhq/kit/src/components/OneKeyAuth/supabase/useSupabaseAuth'; import { useOneKeyAuth } from '@onekeyhq/kit/src/components/OneKeyAuth/useOneKeyAuth'; import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; + +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { JuiceboxClient } from '@onekeyhq/kit-bg/src/services/ServiceKeylessWallet/utils/JuiceboxClient'; import { EOAuthSocialLoginProvider, GOOGLE_OAUTH_CLIENT_IDS, + KEYLESS_SUPABASE_PROJECT_URL, + KEYLESS_SUPABASE_PUBLIC_API_KEY, SUPABASE_PROJECT_URL, SUPABASE_PUBLIC_API_KEY, } from '@onekeyhq/shared/src/consts/authConsts'; @@ -163,6 +168,22 @@ function OneKeyIDApiTests() { {SUPABASE_PUBLIC_API_KEY || '(empty)'} + + + KEYLESS_SUPABASE_PROJECT_URL: + + + {KEYLESS_SUPABASE_PROJECT_URL || '(empty)'} + + + + + KEYLESS_SUPABASE_PUBLIC_API_KEY: + + + {KEYLESS_SUPABASE_PUBLIC_API_KEY || '(empty)'} + + GOOGLE_OAUTH_CLIENT_WEB: @@ -490,28 +511,95 @@ function OneKeyIDApiTests() { > Refresh Session + + + Juicebox Test + + + + + + Onboarding Test diff --git a/packages/shared/src/consts/authConsts.ts b/packages/shared/src/consts/authConsts.ts index d8c7e29e475e..f2b16552fdb1 100644 --- a/packages/shared/src/consts/authConsts.ts +++ b/packages/shared/src/consts/authConsts.ts @@ -175,6 +175,42 @@ export const SUPABASE_PUBLIC_API_KEY = IS_DEV 'sb_publishable_bnNx0b2QZENMm1OLNAyHeQ_FLagwrqN' // local test : 'sb_publishable_bnNx0b2QZENMm1OLNAyHeQ_FLagwrqN'; +export const KEYLESS_SUPABASE_PROJECT_URL = + 'https://wtspqckturkzhstyjabx.supabase.co'; +export const KEYLESS_SUPABASE_PUBLIC_API_KEY = + 'sb_publishable_So24RIupCcXUHaKo1gM4VA_uOBbgjoN'; + +type IJuiceBoxRealmConfig = { + id: string; + address: string; + public_key?: string; +}; + +type IJuiceBoxConfigJSON = { + realms: IJuiceBoxRealmConfig[]; + register_threshold: number; + recover_threshold: number; + pin_hashing_mode: 'Standard2019' | 'FastInsecure'; +}; + +export const JUICEBOX_AUTH_SERVER = 'https://juicebox.onekeytest.com'; +export const JUICEBOX_CONFIG: IJuiceBoxConfigJSON = { + realms: [ + { + id: '37ce3a59ff08d57b77bac0b8451ff2d8', + address: 'https://juicebox-sw-realm-a.onekeytest.com', + }, + { + id: '6b47cc201434428be7beee2190f95685', + address: 'https://juicebox-sw-realm-b.onekeytest.com', + }, + ], + register_threshold: 2, // At least 2 realms must succeed to register + recover_threshold: 2, // At least 2 realms must succeed to recover + pin_hashing_mode: 'Standard2019', +}; +export const JUICEBOX_ALLOWED_GUESSES = 10; // Number of allowed PIN guess attempts + // Supabase OAuth Providers // https://supabase.com/dashboard/project/_/auth/providers diff --git a/packages/shared/src/storage/SupabaseStorage/consts.ts b/packages/shared/src/storage/SupabaseStorage/consts.ts index a616ddd65478..8435f584005c 100644 --- a/packages/shared/src/storage/SupabaseStorage/consts.ts +++ b/packages/shared/src/storage/SupabaseStorage/consts.ts @@ -8,7 +8,7 @@ export const SUPABASE_STORAGE_KEY_PREFIX = 'OneKeySupabaseAuth__'; // Extract project ref from SUPABASE_PROJECT_URL // URL format: https://.supabase.co -export function getSupabaseProjectRef(): string { +function getSupabaseProjectRef(): string { const match = SUPABASE_PROJECT_URL.match(/https?:\/\/([^.]+)\.supabase\.co/); return match?.[1] || ''; } diff --git a/yarn.lock b/yarn.lock index 25a4947bcba3..eef77802b816 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7630,6 +7630,7 @@ __metadata: jest-html-reporter: "npm:^3.10.2" jotai: "npm:^2.5.0" js-md5: "npm:^0.8.3" + juicebox-sdk: "npm:^0.3.4" lightweight-charts: "npm:^3.8.0" long: "npm:^5.2.1" lru-cache: "npm:^10.2.0" @@ -29224,6 +29225,13 @@ __metadata: languageName: node linkType: hard +"juicebox-sdk@npm:^0.3.4": + version: 0.3.4 + resolution: "juicebox-sdk@npm:0.3.4" + checksum: 10/c5531e7da88ab80a5af9409cdc3d3219cfd766830b29b3ff39c21bf646c8afef080c4d1200aa0f809a7bc5c94e81eb39d1566c2046835399524597e00f3c6526 + languageName: node + linkType: hard + "jwt-decode@npm:^4.0.0": version: 4.0.0 resolution: "jwt-decode@npm:4.0.0" From da96db6a4fe774761d46a2f844439f89f4f86d87 Mon Sep 17 00:00:00 2001 From: morizon Date: Wed, 31 Dec 2025 19:22:26 +0800 Subject: [PATCH 50/66] feat: implement juicebox api calls and optimize pin verification error handling --- .../ServiceKeylessWallet.ts | 235 ++++++++---------- .../utils/JuiceboxClient.ts | 59 +++-- .../KeylessWallet/useKeylessWallet.tsx | 15 +- .../container/PasswordSetupContainer.tsx | 23 +- .../container/PasswordVerifyContainer.tsx | 24 +- .../pages/KeylessOnboardingDebugPanel.tsx | 2 +- 6 files changed, 195 insertions(+), 163 deletions(-) diff --git a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts index 698dba27edef..ba1c5c1f569f 100644 --- a/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts +++ b/packages/kit-bg/src/services/ServiceKeylessWallet/ServiceKeylessWallet.ts @@ -36,7 +36,7 @@ import keylessWalletUtils from '@onekeyhq/shared/src/keylessWallet/keylessWallet import shamirUtils from '@onekeyhq/shared/src/keylessWallet/shamirUtils'; import { appLocale } from '@onekeyhq/shared/src/locale/appLocale'; import { ETranslations } from '@onekeyhq/shared/src/locale/enum/translations'; -import appStorage from '@onekeyhq/shared/src/storage/appStorage'; +import { EOnboardingV2OneKeyIDLoginMode } from '@onekeyhq/shared/src/routes'; import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; import bufferUtils from '@onekeyhq/shared/src/utils/bufferUtils'; import type { IAvatarInfo } from '@onekeyhq/shared/src/utils/emojiUtils'; @@ -1109,53 +1109,6 @@ class ServiceKeylessWallet extends ServiceBase { } } - private static readonly MOCKED_KEYLESS_SHARES_STORAGE_KEY = - 'keyless_mocked_shares'; - - private async getMockedKeylessShares(): Promise<{ - [ownerId: string]: { - backendShare: IKeylessBackendShare | undefined; - juiceboxShare: IKeylessJuiceboxShare | undefined; - }; - }> { - const data = await appStorage.getItem( - ServiceKeylessWallet.MOCKED_KEYLESS_SHARES_STORAGE_KEY, - ); - if (!data) { - return {}; - } - try { - return JSON.parse(data) as { - [ownerId: string]: { - backendShare: IKeylessBackendShare | undefined; - juiceboxShare: IKeylessJuiceboxShare | undefined; - }; - }; - } catch { - return {}; - } - } - - private async saveMockedKeylessShares(shares: { - [ownerId: string]: { - backendShare: IKeylessBackendShare | undefined; - juiceboxShare: IKeylessJuiceboxShare | undefined; - }; - }): Promise { - await appStorage.setItem( - ServiceKeylessWallet.MOCKED_KEYLESS_SHARES_STORAGE_KEY, - JSON.stringify(shares), - ); - } - - @backgroundMethod() - @toastIfError() - async clearMockedKeylessShares(): Promise { - await appStorage.removeItem( - ServiceKeylessWallet.MOCKED_KEYLESS_SHARES_STORAGE_KEY, - ); - } - buildKeylessOwnerIdFromSocialToken(params: { token: string }): string { const { token } = params; const decodedToken = stringUtils.decodeJWT(token) as ISupabaseJWTPayload; @@ -1200,58 +1153,135 @@ class ServiceKeylessWallet extends ServiceBase { private async apiGetKeylessBackendShare(params: { token: string; }): Promise { - await timerUtils.wait(1500, { devOnly: true }); const { token } = params; - // verify token by supabase SDK, make sure token is valid and generated by Google or Apple - // decode token to get social account id - // hash social account id by env salt and KMS + + const client = await this.getClient(EServiceEndpointEnum.Prime); + const res = await client.post>( + '/prime/v1/keyless-wallet/getKeylessBackendShare', + { + token, + }, + ); + + const backendShareStr = res?.data?.data; + if (backendShareStr) { + try { + return JSON.parse(backendShareStr) as IKeylessBackendShare; + } catch (e) { + console.error('Failed to parse keyless backend share', e); + } + } + return null; + } + + @backgroundMethod() + @toastIfError() + async apiUploadKeylessBackendShare(params: { + token: string; + encryptedMnemonic: string; + backendShare: string; + juiceboxShareX: number; + }): Promise { + const { token, encryptedMnemonic, backendShare, juiceboxShareX } = params; const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); // TODO: Replace with real API call - // For now, return from mock cache - const mockedShares = await this.getMockedKeylessShares(); - return mockedShares[ownerId]?.backendShare || null; + // For now, save to mock cache + const backendShareData: IKeylessBackendShare = { + ownerId, + encryptedMnemonic, // TODO to base64 + backendShare, + juiceboxShareX, + }; + + const client = await this.getClient(EServiceEndpointEnum.Prime); + const res = await client.post>( + '/prime/v1/keyless-wallet/createKeylessBackendShare', + { + token, + keylessBackendShare: JSON.stringify(backendShareData), // TODO encrypt + }, + ); + + if (res?.data?.data?.ok === true) { + return backendShareData; + } + + throw new OneKeyLocalError('Failed to upload keyless backend share'); } private async apiGetKeylessJuiceboxShare(params: { token: string; pin: string; }): Promise { - await timerUtils.wait(1500, { devOnly: true }); const { token, pin } = params; - await this.apiVerifyKeylessJuiceboxPin({ token, pin }); + + if (!token) { + throw new OneKeyLocalError('Missing token'); + } + + if (!pin) { + throw new OneKeyLocalError('Missing pin'); + } + const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); - // TODO: Replace with real API call - // exchange juicebox token from onekey auth server - // get juicebox share from juicebox network - // For now, return from mock cache - const mockedShares = await this.getMockedKeylessShares(); - return mockedShares[ownerId]?.juiceboxShare || null; + + const { JuiceboxClient } = await import('./utils/JuiceboxClient'); + const juiceboxClient = new JuiceboxClient(); + await juiceboxClient.exchangeToken(token); + try { + const secret = await juiceboxClient.recover({ + pin, + userInfo: ownerId, + }); + + const parts = secret.split('--'); + const backendShareXStr = parts.pop(); + if (!backendShareXStr) { + throw new OneKeyLocalError( + 'Failed to get keyless juicebox share: backendShareXStr is empty', + ); + } + const backendShareX = parseInt(backendShareXStr || '0', 10); + const juiceboxShare = parts.join(''); + if (!juiceboxShare) { + throw new OneKeyLocalError( + 'Failed to get keyless juicebox share: juiceboxShare is empty', + ); + } + return { + ownerId, + pin, + juiceboxShare, + backendShareX, + }; + } catch (e) { + const error = e as { guesses_remaining: number; reason: number }; + console.error(e); + throw e; + } } - // apiVerifyKeylessJuiceboxPin @backgroundMethod() @toastIfError() async apiVerifyKeylessJuiceboxPin(params: { token: string; pin: string; refreshToken?: string; + mode?: EOnboardingV2OneKeyIDLoginMode; }) { - await timerUtils.wait(1500, { devOnly: true }); - const { token, pin, refreshToken } = params; + const { token, pin, refreshToken, mode } = params; const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); - // TODO: Replace with real API call - // For now, verify PIN from mock cache - const mockedShares = await this.getMockedKeylessShares(); - const juiceboxShare = mockedShares[ownerId]?.juiceboxShare; - if (!juiceboxShare) { - throw new OneKeyLocalError('Juicebox share not found'); - } - if (juiceboxShare?.pin !== pin) { - throw new OneKeyLocalError('Invalid PIN'); - } + + await this.apiGetKeylessJuiceboxShare({ + token, + pin, + }); // Save refresh token to secure storage - if (refreshToken) { + if ( + refreshToken && + mode === EOnboardingV2OneKeyIDLoginMode.KeylessVerifyPinOnly + ) { await keylessRefreshTokenStorage.saveRefreshTokenToStorage({ ownerId, refreshToken, @@ -1260,37 +1290,6 @@ class ServiceKeylessWallet extends ServiceBase { } } - @backgroundMethod() - @toastIfError() - async apiUploadKeylessBackendShare(params: { - token: string; - encryptedMnemonic: string; - backendShare: string; - juiceboxShareX: number; - }): Promise { - await timerUtils.wait(1500, { devOnly: true }); - const { token, encryptedMnemonic, backendShare, juiceboxShareX } = params; - const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); - // TODO: Replace with real API call - // For now, save to mock cache - const backendShareData: IKeylessBackendShare = { - ownerId, - encryptedMnemonic, - backendShare, - juiceboxShareX, - }; - const mockedShares = await this.getMockedKeylessShares(); - if (!mockedShares[ownerId]) { - mockedShares[ownerId] = {} as { - backendShare: IKeylessBackendShare; - juiceboxShare: IKeylessJuiceboxShare; - }; - } - mockedShares[ownerId].backendShare = backendShareData; - await this.saveMockedKeylessShares(mockedShares); - return backendShareData; - } - @backgroundMethod() @toastIfError() async apiUploadKeylessJuiceboxShare(params: { @@ -1299,7 +1298,6 @@ class ServiceKeylessWallet extends ServiceBase { juiceboxShare: string; backendShareX: number; }): Promise { - await timerUtils.wait(1500, { devOnly: true }); const { token, pin, juiceboxShare, backendShareX } = params; const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); // TODO: Replace with real API call @@ -1312,15 +1310,6 @@ class ServiceKeylessWallet extends ServiceBase { juiceboxShare, backendShareX, }; - // const mockedShares = await this.getMockedKeylessShares(); - // if (!mockedShares[ownerId]) { - // mockedShares[ownerId] = {} as { - // backendShare: IKeylessBackendShare; - // juiceboxShare: IKeylessJuiceboxShare; - // }; - // } - // mockedShares[ownerId].juiceboxShare = juiceboxShareData; - // await this.saveMockedKeylessShares(mockedShares); const { JuiceboxClient } = await import('./utils/JuiceboxClient'); const juiceboxClient = new JuiceboxClient(); @@ -1435,11 +1424,6 @@ class ServiceKeylessWallet extends ServiceBase { // check if keyless wallet is initialized const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); - const mockedShares = await this.getMockedKeylessShares(); - const existingShares = mockedShares[ownerId]; - if (!existingShares?.backendShare || !existingShares?.juiceboxShare) { - throw new OneKeyLocalError('Keyless wallet not created'); - } // Get backend share from server const backendShareData = await this.apiGetKeylessBackendShare({ token }); @@ -1522,9 +1506,8 @@ class ServiceKeylessWallet extends ServiceBase { throw new OneKeyLocalError('pin is required'); } const ownerId = this.buildKeylessOwnerIdFromSocialToken({ token }); - const mockedShares = await this.getMockedKeylessShares(); - const existingShares = mockedShares[ownerId]; - if (existingShares?.backendShare || existingShares?.juiceboxShare) { + const isCreated = await this.isKeylessWalletCreatedOnServer({ token }); + if (isCreated) { throw new OneKeyLocalError('Keyless wallet already created'); } let mnemonic = ''; diff --git a/packages/kit-bg/src/services/ServiceKeylessWallet/utils/JuiceboxClient.ts b/packages/kit-bg/src/services/ServiceKeylessWallet/utils/JuiceboxClient.ts index 93e54e72cbdd..1c37d01952c6 100644 --- a/packages/kit-bg/src/services/ServiceKeylessWallet/utils/JuiceboxClient.ts +++ b/packages/kit-bg/src/services/ServiceKeylessWallet/utils/JuiceboxClient.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import { Client, Configuration } from 'juicebox-sdk'; +import { isNil } from 'lodash'; import { JUICEBOX_ALLOWED_GUESSES, @@ -150,36 +151,48 @@ export class JuiceboxClient { pin: string; userInfo: string; // ownerId }): Promise { - const { pin, userInfo } = params; + try { + const { pin, userInfo } = params; - // Validate token cache is not empty - if (this.juiceboxTokenCache.size === 0) { - throw new OneKeyLocalError( - 'Juicebox token cache is empty, please call exchangeToken first', - ); - } + // Validate token cache is not empty + if (this.juiceboxTokenCache.size === 0) { + throw new OneKeyLocalError( + 'Juicebox token cache is empty, please call exchangeToken first', + ); + } - // Convert strings to Uint8Array - const pinBytes = bufferUtils.utf8ToBytes(pin); - const userInfoBytes = bufferUtils.utf8ToBytes(userInfo); + // Convert strings to Uint8Array + const pinBytes = bufferUtils.utf8ToBytes(pin); + const userInfoBytes = bufferUtils.utf8ToBytes(userInfo); - // Call SDK recover method - const recoveredSecret: Uint8Array = await this.client.recover( - pinBytes, - userInfoBytes, - ); + // Call SDK recover method + const recoveredSecret: Uint8Array = await this.client.recover( + pinBytes, + userInfoBytes, + ); - if (!recoveredSecret?.length) { - throw new OneKeyLocalError('Recovery failed: Empty secret.'); - } + if (!recoveredSecret?.length) { + throw new OneKeyLocalError('Recovery failed: Empty secret.'); + } - // Convert recovered Uint8Array back to utf8 string - const secretUtf8 = bufferUtils.bytesToUtf8(recoveredSecret); + // Convert recovered Uint8Array back to utf8 string + const secretUtf8 = bufferUtils.bytesToUtf8(recoveredSecret); - // Clear token cache after successful recovery - this.clearTokenCache(); + // Clear token cache after successful recovery + this.clearTokenCache(); - return secretUtf8; + return secretUtf8; + } catch (e) { + const error = e as + | { guesses_remaining: number; reason: number } + | undefined; + if (!isNil(error?.guesses_remaining)) { + throw new OneKeyLocalError( + `Incorrect PIN, you have ${error?.guesses_remaining} guesses remaining`, + ); + } + throw e; + } } /** diff --git a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx index 8dac88cd71a1..2cc4fc70a55d 100644 --- a/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx +++ b/packages/kit/src/components/KeylessWallet/useKeylessWallet.tsx @@ -43,11 +43,12 @@ import { useAccountSelectorActions } from '../../states/jotai/contexts/accountSe import { useOneKeyAuth } from '../OneKeyAuth/useOneKeyAuth'; export function useKeylessWalletFeatureIsEnabled(): boolean { - const [devSettings] = useDevSettingsPersistAtom(); - return ( - devSettings.enabled && - devSettings.settings?.isKeylessWalletFeatureEnabled === true - ); + // const [devSettings] = useDevSettingsPersistAtom(); + // return ( + // devSettings.enabled && + // devSettings.settings?.isKeylessWalletFeatureEnabled === true + // ); + return true; } export function useKeylessWalletExistsLocal(): boolean { @@ -616,6 +617,9 @@ export function useKeylessWallet() { screen: EOnboardingV2Routes.OnboardingV2, params: { screen: EOnboardingPagesV2.VerifyPin, + params: { + mode: EOnboardingV2OneKeyIDLoginMode.KeylessCreateOrRestore, + }, }, }); } else { @@ -857,6 +861,7 @@ export function useKeylessWallet() { token, pin, refreshToken, + mode, }, ); diff --git a/packages/kit/src/components/Password/container/PasswordSetupContainer.tsx b/packages/kit/src/components/Password/container/PasswordSetupContainer.tsx index a2ea73f5d7ee..a6a1dbcbccd7 100644 --- a/packages/kit/src/components/Password/container/PasswordSetupContainer.tsx +++ b/packages/kit/src/components/Password/container/PasswordSetupContainer.tsx @@ -30,7 +30,7 @@ import PasswordSetup from '../components/PasswordSetup'; import type { IPasswordSetupForm } from '../components/PasswordSetup'; interface IPasswordSetupProps { - onSetupRes: (password: string) => void; + onSetupRes: (password: string) => void | Promise; pageMode?: boolean; } @@ -109,9 +109,15 @@ const PasswordSetupContainer = ({ Toast.success({ title: intl.formatMessage({ id: ETranslations.auth_passcode_set }), }); - setTimeout(() => { - onSetupRes(setUpPasswordRes); - }); + + if (pageMode) { + await onSetupRes(setUpPasswordRes); + } else { + setTimeout(() => { + void onSetupRes(setUpPasswordRes); + }); + } + // Dialog.show({ // title: intl.formatMessage({ // id: ETranslations.auth_Passcode_protection, @@ -165,7 +171,14 @@ const PasswordSetupContainer = ({ setLoading(false); } }, - [intl, isBiologyAuthSwitchOn, isSupport, onSetupRes, setWebAuthEnable], + [ + intl, + isBiologyAuthSwitchOn, + isSupport, + onSetupRes, + pageMode, + setWebAuthEnable, + ], ); return ( diff --git a/packages/kit/src/components/Password/container/PasswordVerifyContainer.tsx b/packages/kit/src/components/Password/container/PasswordVerifyContainer.tsx index 7e4fa692dbff..49ae832a094b 100644 --- a/packages/kit/src/components/Password/container/PasswordVerifyContainer.tsx +++ b/packages/kit/src/components/Password/container/PasswordVerifyContainer.tsx @@ -200,7 +200,13 @@ const PasswordVerifyContainer = ({ if (isExtLockNoCachePassword) { const result = await checkWebAuth(); if (result) { - await onVerifyRes(''); + if (pageMode) { + await onVerifyRes(''); + } else { + setTimeout(() => { + void onVerifyRes(''); + }); + } setPasswordAtom((v) => ({ ...v, passwordVerifyStatus: { value: EPasswordVerifyStatus.VERIFIED }, @@ -223,7 +229,13 @@ const PasswordVerifyContainer = ({ }); } if (biologyAuthRes) { - await onVerifyRes(biologyAuthRes); + if (pageMode) { + await onVerifyRes(biologyAuthRes); + } else { + setTimeout(() => { + void onVerifyRes(biologyAuthRes); + }); + } setPasswordAtom((v) => ({ ...v, passwordVerifyStatus: { value: EPasswordVerifyStatus.VERIFIED }, @@ -323,7 +335,13 @@ const PasswordVerifyContainer = ({ dismissKeyboard(); await timerUtils.wait(0); } - await onVerifyRes(verifiedPassword); + if (pageMode) { + await onVerifyRes(verifiedPassword); + } else { + setTimeout(() => { + void onVerifyRes(verifiedPassword); + }); + } setPasswordAtom((v) => ({ ...v, passwordVerifyStatus: { value: EPasswordVerifyStatus.VERIFIED }, diff --git a/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx b/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx index e5448f6a9d43..ecd611dd3986 100644 --- a/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/KeylessOnboardingDebugPanel.tsx @@ -77,7 +77,7 @@ export function KeylessOnboardingDebugPanel() { + +
+ +
+

Login failed

+

Something went wrong during login.

+
+

${escapeHtml(errorMessage)}

+
+

Click the button below if this window does not close automatically.

+
+ +
+