diff --git a/.env.example b/.env.example index 8c21ed984847..4296c6e125be 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,13 @@ COVALENT_KEY= JPUSH_KEY= JPUSH_CHANNEL= +# Onramper Headless SDK production pair (native iOS buy, OK-59538). +# Publishable pk_... key + client id — NOT the backend partner secret. +# CI injects via GitHub secrets; for local production-profile builds put the +# values in the git-ignored root .env (never in .env.expo / .env.version). +ONRAMPER_CLIENT_ID= +ONRAMPER_API_KEY= + # for webpack analyzer ENABLE_ANALYZER= ENABLE_ANALYZER_HTML_REPORT= diff --git a/.github/workflows/release-ios.yml b/.github/workflows/release-ios.yml index 240a54f9ff80..0d1664d900fb 100644 --- a/.github/workflows/release-ios.yml +++ b/.github/workflows/release-ios.yml @@ -46,6 +46,8 @@ jobs: additional_env: | NO_FLIPPER=1 SENTRY_DISABLE_AUTO_UPLOAD=false + ONRAMPER_CLIENT_ID=${{ secrets.ONRAMPER_CLIENT_ID }} + ONRAMPER_API_KEY=${{ secrets.ONRAMPER_API_KEY }} env_file_name: '.env.expo' sentry_project: '' covalent_key: ${{ secrets.COVALENT_KEY }} diff --git a/apps/mobile/ios/OneKeyWallet.xcodeproj/project.pbxproj b/apps/mobile/ios/OneKeyWallet.xcodeproj/project.pbxproj index dce58fd5d8d3..a79fa58d12ca 100644 --- a/apps/mobile/ios/OneKeyWallet.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/OneKeyWallet.xcodeproj/project.pbxproj @@ -272,6 +272,7 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "OneKeyWallet" */; buildPhases = ( + 0288C121D3EE8444D1801CDC /* [OneKey] Assert App Attest environment */, 860344BEAD92EB4C2606DBCA /* [CP] Check Pods Manifest.lock */, D8981D2998B0CB74725DB299 /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, @@ -398,6 +399,26 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 0288C121D3EE8444D1801CDC /* [OneKey] Assert App Attest environment */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "[OneKey] Assert App Attest environment"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Onramper production credentials ship only in Release JS bundles\n# (platformEnv.isDev picks the staging pair in Debug), and the SDK's App\n# Attest assertion only passes when the entitlement environment matches the\n# key's environment. Fail the build instead of shipping a Release app whose\n# attestation can never succeed (OK-59538).\nif [ \"${CONFIGURATION}\" = \"Release\" ] && [ \"${ONEKEY_APPATTEST_ENVIRONMENT}\" != \"production\" ]; then\n echo \"error: Release builds require ONEKEY_APPATTEST_ENVIRONMENT=production (got '${ONEKEY_APPATTEST_ENVIRONMENT}'). App Attest must match the Onramper production credentials (OK-59538).\"\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; 2FA0D2DF4C8B4E3B9FCE32CA /* Upload Debug Symbols to Sentry */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -419,12 +440,14 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-OneKeyWallet/Pods-OneKeyWallet-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/OnramperReactNative/OnramperSDK.framework/OnramperSDK", "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", "${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeLiteCard/GPChannelSDKCore.framework/GPChannelSDKCore", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OnramperSDK.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GPChannelSDKCore.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", @@ -690,12 +713,13 @@ "FB_SONARKIT_ENABLED=1", ); INFOPLIST_FILE = OneKeyWallet/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.5; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; + ONEKEY_APPATTEST_ENVIRONMENT = development; ONLY_ACTIVE_ARCH = NO; OTHER_LDFLAGS = ( "$(inherited)", @@ -729,12 +753,13 @@ ENABLE_NATIVE_BACKGROUND_THREAD = true; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; INFOPLIST_FILE = OneKeyWallet/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.5; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; + ONEKEY_APPATTEST_ENVIRONMENT = production; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "$(inherited)", @@ -939,7 +964,7 @@ INFOPLIST_FILE = ServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 15.5; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD = clang; LDPLUSPLUS = "clang++"; LD_RUNPATH_SEARCH_PATHS = ( @@ -984,7 +1009,7 @@ INFOPLIST_FILE = ServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 15.5; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD = clang; LDPLUSPLUS = "clang++"; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/apps/mobile/ios/OneKeyWallet/OneKeyWallet.entitlements b/apps/mobile/ios/OneKeyWallet/OneKeyWallet.entitlements index cefb9edae8ce..b8c82f06a17a 100644 --- a/apps/mobile/ios/OneKeyWallet/OneKeyWallet.entitlements +++ b/apps/mobile/ios/OneKeyWallet/OneKeyWallet.entitlements @@ -18,6 +18,8 @@ applinks:perps.onekeytest.com applinks:swap.onekey.so + com.apple.developer.devicecheck.appattest-environment + $(ONEKEY_APPATTEST_ENVIRONMENT) com.apple.developer.icloud-container-environment Production com.apple.developer.icloud-container-identifiers diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock index aa7d90a918ee..6356cffdabac 100644 --- a/apps/mobile/ios/Podfile.lock +++ b/apps/mobile/ios/Podfile.lock @@ -65,7 +65,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - AutoSizeInput (3.0.78): + - AutoSizeInput (3.0.82): - boost - DoubleConversion - fast_float @@ -157,7 +157,7 @@ PODS: - ExpoModulesCore - SPAlert (~> 4.2) - SPIndicator (~> 1.6) - - ChartWebview (3.0.78): + - ChartWebview (3.0.82): - boost - DoubleConversion - fast_float @@ -216,7 +216,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - CloudKitModule (3.0.78): + - CloudKitModule (3.0.82): - boost - DoubleConversion - fast_float @@ -247,8 +247,8 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - CocoaLumberjack/Core (3.9.0) - - CocoaLumberjack/Swift (3.9.0): + - CocoaLumberjack/Core (3.9.1) + - CocoaLumberjack/Swift (3.9.1): - CocoaLumberjack/Core - DnsLookup (3.0.78): - boost @@ -474,7 +474,7 @@ PODS: - JPushRN (3.2.1): - React - JuiceboxSdk (0.3.2) - - KeychainModule (3.0.78): + - KeychainModule (3.0.82): - boost - DoubleConversion - fast_float @@ -547,9 +547,9 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - MMKV (2.2.4): - - MMKVCore (~> 2.2.4) - - MMKVCore (2.2.4) + - MMKV (2.3.0): + - MMKVCore (~> 2.3.0) + - MMKVCore (2.3.0) - MultiplatformBleAdapter (0.2.0) - NetworkInfo (3.0.78): - boost @@ -579,14 +579,14 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - NitroMmkv (4.1.0): + - NitroMmkv (4.2.0): - boost - DoubleConversion - fast_float - fmt - glog - hermes-engine - - MMKVCore (= 2.2.4) + - MMKVCore (= 2.3.0) - NitroModules - RCT-Folly - RCT-Folly/Fabric @@ -610,7 +610,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - NitroModules (0.33.2): + - NitroModules (0.35.2): - boost - DoubleConversion - fast_float @@ -639,6 +639,35 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga + - OnramperReactNative (1.1.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - NitroModules + - 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 - OpenSSL-Universal (3.6.2000) - Pbkdf2 (3.0.78): - boost @@ -668,7 +697,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - PerpDepthBar (3.0.78): + - PerpDepthBar (3.0.82): - boost - DoubleConversion - fast_float @@ -3608,7 +3637,7 @@ PODS: - React-perflogger (= 0.81.5) - React-utils (= 0.81.5) - SocketRocket - - ReactNativeAppUpdate (3.0.78): + - ReactNativeAppUpdate (3.0.82): - boost - DoubleConversion - fast_float @@ -3639,7 +3668,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeBundleCrypto (3.0.78): + - ReactNativeBundleCrypto (3.0.82): - boost - DoubleConversion - fast_float @@ -3670,7 +3699,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeBundleUpdate (3.0.78): + - ReactNativeBundleUpdate (3.0.82): - boost - DoubleConversion - fast_float @@ -3733,7 +3762,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - ReactNativeCheckBiometricAuthChanged (3.0.78): + - ReactNativeCheckBiometricAuthChanged (3.0.82): - boost - DoubleConversion - fast_float @@ -3764,7 +3793,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeDeviceUtils (3.0.78): + - ReactNativeDeviceUtils (3.0.82): - boost - DoubleConversion - fast_float @@ -3823,7 +3852,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - ReactNativeGetRandomValues (3.0.78): + - ReactNativeGetRandomValues (3.0.82): - boost - DoubleConversion - fast_float @@ -3883,7 +3912,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeNativeLogger (3.0.78): + - ReactNativeNativeLogger (3.0.82): - boost - CocoaLumberjack/Swift (~> 3.8) - DoubleConversion @@ -3944,7 +3973,7 @@ PODS: - Yoga - ReactNativePasskeys (0.3.3): - ExpoModulesCore - - ReactNativePerfMemory (3.0.78): + - ReactNativePerfMemory (3.0.82): - boost - DoubleConversion - fast_float @@ -3975,7 +4004,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativePerfStats (3.0.78): + - ReactNativePerfStats (3.0.82): - boost - DoubleConversion - fast_float @@ -4006,7 +4035,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeRangeDownloader (3.0.78): + - ReactNativeRangeDownloader (3.0.82): - boost - DoubleConversion - fast_float @@ -4036,8 +4065,9 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeNativeLogger - SocketRocket + - SSZipArchive (= 2.5.5) - Yoga - - ReactNativeSplashScreen (3.0.78): + - ReactNativeSplashScreen (3.0.82): - boost - DoubleConversion - fast_float @@ -4068,7 +4098,7 @@ PODS: - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeZipArchive (3.0.78): + - ReactNativeZipArchive (3.0.82): - boost - DoubleConversion - fast_float @@ -4639,7 +4669,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - ScrollGuard (3.0.78): + - ScrollGuard (3.0.82): - boost - DoubleConversion - fast_float @@ -4677,7 +4707,7 @@ PODS: - SDWebImageWebPCoder (0.14.6): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) - - SegmentSlider (3.0.78): + - SegmentSlider (3.0.82): - boost - DoubleConversion - fast_float @@ -4708,7 +4738,7 @@ PODS: - SocketRocket - Yoga - Sentry/HybridSDK (8.56.2) - - Skeleton (3.0.78): + - Skeleton (3.0.82): - boost - DoubleConversion - fast_float @@ -4897,6 +4927,7 @@ DEPENDENCIES: - NetworkInfo (from `../../../node_modules/react-native-network-info`) - NitroMmkv (from `../../../node_modules/react-native-mmkv`) - NitroModules (from `../../../node_modules/react-native-nitro-modules`) + - "OnramperReactNative (from `../../../node_modules/@onramper/onramper-react-native`)" - Pbkdf2 (from `../../../node_modules/react-native-fast-pbkdf2`) - "PerpDepthBar (from `../../../node_modules/@onekeyfe/react-native-perp-depth-bar`)" - Ping (from `../../../node_modules/react-native-ping`) @@ -5171,6 +5202,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native-mmkv" NitroModules: :path: "../../../node_modules/react-native-nitro-modules" + OnramperReactNative: + :path: "../../../node_modules/@onramper/onramper-react-native" Pbkdf2: :path: "../../../node_modules/react-native-fast-pbkdf2" PerpDepthBar: @@ -5427,15 +5460,15 @@ SPEC CHECKSUMS: AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063 AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f AsyncStorage: c412e811f40580575ae1349fe3a00ba20abbd7b9 - AutoSizeInput: 058768d2d7b2eba06620d9a3fabe260d08de1218 + AutoSizeInput: ad7de4e45d1318b2c17f9616f96c66c2ead6a6b3 BackgroundThread: 2716941fdd405d1e2b782579834ded774968bd62 BleUtils: 3ab85e1bcf3c7a5594488ee144a0b9bbf4537d55 boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 Burnt: e3a3397e26172fca31a59bb27421475a58068836 - ChartWebview: 91e4e32693e817baee0d449695939d98a66fbb8e + ChartWebview: 66a9aa9a840da36b1c0b67b984d465e026faf7f9 CloudFs: c80cf2add5f2a39911cdaad7911482304252da98 - CloudKitModule: 7c968a39e47df73e85baa88f7155c25c6a20c20e - CocoaLumberjack: 5644158777912b7de7469fa881f8a3f259c2512a + CloudKitModule: 8615ddebce2f179934b799f3a13e4b6888ae9c7b + CocoaLumberjack: e4ba3b414dfca8c1916c6303d37f63b3a95134c6 DnsLookup: da5729e2c98cb5c60eeed3b8b09b74a240766983 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb EMASCurl: d75387e1ce9dec1a75cd25cb33c7a7e7bf21997f @@ -5485,19 +5518,20 @@ SPEC CHECKSUMS: JCoreRN: d985de509185b381c177fde4bb6bfc686089fdbd JPushRN: 807bc962b2f25860e5c0caa5fcb7b4910572b890 JuiceboxSdk: b2222491ce92b263694c217ea202a54b66c5ec5f - KeychainModule: b4590cd224fef4014d51de5c3cfc579f17ed0c29 + KeychainModule: f4e6a8820c528229f6805a8a28b97a29cb924808 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 lottie-ios: a881093fab623c467d3bce374367755c272bdd59 lottie-react-native: 86fa7488b1476563f41071244aa73d5b738faf19 - MMKV: 1a8e7dbce7f9cad02c52e1b1091d07bd843aefaf - MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df + MMKV: c953dbaac0da392c24b005e763c03ce2638b4ed7 + MMKVCore: d078dce7d6586a888b2c2ef5343b6242678e3ee8 MultiplatformBleAdapter: b1fddd0d499b96b607e00f0faa8e60648343dc1d NetworkInfo: 8f477bda9569e9f5cf6ac784df616540037fb4a4 - NitroMmkv: ce1df9b9f0e06dfbde2455d863047e0411fceb6e - NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3 + NitroMmkv: f0a5804b437ecda18aaf0c75649964343438f083 + NitroModules: 17f56ebb3581e74931c870a789834582ce75470c + OnramperReactNative: f081098a6e4926d4c61dad22be6f8eefecd3eb6d OpenSSL-Universal: ecee7b138fa75a74ecf00d7ffd248fb584739b9e Pbkdf2: 76a41c21cebe8b04bd539c0070a7dc6a69f50068 - PerpDepthBar: ea764fd9ca6dde336b3b6ca292e783e2b9c66c69 + PerpDepthBar: 78c75a72a51ee4361d776a0eed5f9b6234ec9004 Ping: fa822267f2829fc55b444fe503ce3022463d875e PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PurchasesHybridCommon: 097af88db3cb39b415bef2f3140e6b3324899756 @@ -5581,23 +5615,23 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 1bcd3527ac0390a1c898c114f81ff954be35ed79 ReactCodegen: 0c47bcad3576258ef1a91e2ec1fd43afae72e85f ReactCommon: 5f0e5c09a64a2717215dd84380e1a747810406f2 - ReactNativeAppUpdate: 064c5b9d83740a8847978a82e497f238c6938ae1 - ReactNativeBundleCrypto: 4d33c409f555b35ebc9fc2e96bc874e94a1c17a0 - ReactNativeBundleUpdate: d0003f5c258b79d90182d44d4bc363f37b1aaafa + ReactNativeAppUpdate: f41df92ea16313d036efac53f0170a38e4ce9a1d + ReactNativeBundleCrypto: b423874af32056724e7c8968f62e79c93f8f3c23 + ReactNativeBundleUpdate: 8d2b5033849c060ef2b3154471cdd3a74639433b ReactNativeCameraKit: 84894fc476300e5d3b9f4e34f55ff75050ec5050 - ReactNativeCheckBiometricAuthChanged: 384e28d803c255b276dc11630e8cd5b5a1587658 - ReactNativeDeviceUtils: 39d27fa43c2ba191f3457d6123d778c882af88fd + ReactNativeCheckBiometricAuthChanged: 24fbb79011bc31b6f026c63bf321ee17b2b368d8 + ReactNativeDeviceUtils: 3fefe59390979c625a2b612131ff3c3466e6edfe ReactNativeFs: fa4d6c721fedd16abb5a7eb5c591303f147677b5 - ReactNativeGetRandomValues: 62cb6e978f513af1a23b2f3fae4eba25050ee771 + ReactNativeGetRandomValues: 31ea698091395a2fcf813729574e0fd5b2b7533a ReactNativeLiteCard: 7f5c52161aa5b60540ac46121a9ff5b285466ac7 - ReactNativeNativeLogger: f4e4dd6f92ea5d2178c0ceccd7eacb2c4c6767f8 + ReactNativeNativeLogger: aeda632d0f758a5e1242221dfb96a83c16c469e8 ReactNativeNetworkThrottle: 1a3d096e4e8fa018a9d6a5f9b7def573735c3c5d ReactNativePasskeys: 9e950e8cbf0e7d6aad9df4dcd21cee0efeb4e5cd - ReactNativePerfMemory: 7ef4df212ac2a19e5125deb9f2067ee110235e48 - ReactNativePerfStats: d1368e3a14b5387dea7cc87bca9a566810636f5e - ReactNativeRangeDownloader: fb71689f6c2ccf99ad63b59411a84ee1581bc7ac - ReactNativeSplashScreen: 0bc82cdce113b2f60366b3d0f2a495ff86e13022 - ReactNativeZipArchive: bb4a2b338281c0166bee97142bf59ef9cd124c62 + ReactNativePerfMemory: 13ed620aab5ba2bc970d083ba34bab4a4a0152a5 + ReactNativePerfStats: 211c4857e69ef55cbc8c3a63d9f9deb7fec6e41f + ReactNativeRangeDownloader: 6a105a61ad0f7bf9ae44aade855326d1fc422071 + ReactNativeSplashScreen: b781ac4576c96acfd492909428366ef7e31071ae + ReactNativeZipArchive: c041e528c0c5bb7ebf37d29e29565585aa95224c RealmJS: 1c37c6bdfe060f4caa0f9175aa0eedb962622ee1 RevenueCat: 72d1e14966339bf38c41d9b2841ef64c8fd79646 RNDateTimePicker: cda4c045beca864cebb3209ef9cc4094f974864c @@ -5613,13 +5647,13 @@ SPEC CHECKSUMS: RNSentry: a20c5194cc6d7902683276f399a6eb5206fb9f05 RNSVG: d260beec1775108e1bd065a0ed666ff2c5565158 RNWorklets: 8068c8af4b241eb2c19221310729e4c440bee023 - ScrollGuard: 9c1ba98e33f45adeb5c2161af6cc2916b3e76187 + ScrollGuard: c03a375964f8ff757ae70ae0f1a73ea0b51bd7df SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 - SegmentSlider: e3507345e9bf6a48fd301181765d056a70420512 + SegmentSlider: c96b8e842ffebba97df0c5c683688627bf070398 Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7 - Skeleton: e04f3e3d91865cdb03ef256a095d128688acf3fb + Skeleton: a3412aeb78aef87ea8f38a2dcf90717cec3ac9ed SniConnect: 4093cf48264f2b78081f8cfa8482d7a6453803c5 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 SPAlert: 735da1f16a887e294719217572ce1f936d8c8782 diff --git a/apps/mobile/ios/Podfile.properties.json b/apps/mobile/ios/Podfile.properties.json index 838b2369b2e0..cc16cf807e8d 100644 --- a/apps/mobile/ios/Podfile.properties.json +++ b/apps/mobile/ios/Podfile.properties.json @@ -1,5 +1,6 @@ { "expo.jsEngine": "hermes", "expo.newArchEnabled": "true", - "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true" + "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true", + "ios.deploymentTarget": "16.0" } diff --git a/apps/mobile/package.json b/apps/mobile/package.json index acf4e928abad..733f6fc1435d 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -58,35 +58,36 @@ "@formatjs/intl-locale": "^2.4.47", "@formatjs/intl-pluralrules": "^4.3.3", "@notifee/react-native": "9.1.8", - "@onekeyfe/react-native-app-update": "3.0.78", - "@onekeyfe/react-native-auto-size-input": "3.0.78", + "@onekeyfe/react-native-app-update": "3.0.82", + "@onekeyfe/react-native-auto-size-input": "3.0.82", "@onekeyfe/react-native-background-thread": "3.0.78", "@onekeyfe/react-native-ble-utils": "0.1.6", - "@onekeyfe/react-native-bundle-crypto": "3.0.78", - "@onekeyfe/react-native-bundle-update": "3.0.78", - "@onekeyfe/react-native-chart-webview": "3.0.78", - "@onekeyfe/react-native-check-biometric-auth-changed": "3.0.78", - "@onekeyfe/react-native-cloud-kit-module": "3.0.78", - "@onekeyfe/react-native-device-utils": "3.0.78", - "@onekeyfe/react-native-keychain-module": "3.0.78", + "@onekeyfe/react-native-bundle-crypto": "3.0.82", + "@onekeyfe/react-native-bundle-update": "3.0.82", + "@onekeyfe/react-native-chart-webview": "3.0.82", + "@onekeyfe/react-native-check-biometric-auth-changed": "3.0.82", + "@onekeyfe/react-native-cloud-kit-module": "3.0.82", + "@onekeyfe/react-native-device-utils": "3.0.82", + "@onekeyfe/react-native-keychain-module": "3.0.82", "@onekeyfe/react-native-lite-card": "3.0.78", - "@onekeyfe/react-native-native-logger": "3.0.78", + "@onekeyfe/react-native-native-logger": "3.0.82", "@onekeyfe/react-native-network-throttle": "3.0.78", - "@onekeyfe/react-native-perf-memory": "3.0.78", - "@onekeyfe/react-native-perf-stats": "3.0.78", - "@onekeyfe/react-native-perp-depth-bar": "3.0.78", - "@onekeyfe/react-native-range-downloader": "3.0.78", - "@onekeyfe/react-native-scroll-guard": "3.0.78", - "@onekeyfe/react-native-segment-slider": "3.0.78", - "@onekeyfe/react-native-skeleton": "3.0.78", + "@onekeyfe/react-native-perf-memory": "3.0.82", + "@onekeyfe/react-native-perf-stats": "3.0.82", + "@onekeyfe/react-native-perp-depth-bar": "3.0.82", + "@onekeyfe/react-native-range-downloader": "3.0.82", + "@onekeyfe/react-native-scroll-guard": "3.0.82", + "@onekeyfe/react-native-segment-slider": "3.0.82", + "@onekeyfe/react-native-skeleton": "3.0.82", "@onekeyfe/react-native-sni-connect": "3.0.78", - "@onekeyfe/react-native-splash-screen": "3.0.78", + "@onekeyfe/react-native-splash-screen": "3.0.82", "@onekeyfe/react-native-split-bundle-loader": "3.0.78", "@onekeyfe/react-native-tab-view": "3.0.78", "@onekeyfe/react-native-text-input": "0.3.0", "@onekeyhq/components": "*", "@onekeyhq/kit": "*", "@onekeyhq/shared": "*", + "@onramper/onramper-react-native": "1.1.1", "@phantom/react-native-juicebox-sdk": "0.3.17", "@react-native-async-storage/async-storage": "npm:@onekeyfe/react-native-async-storage@3.0.78", "@react-native-community/netinfo": "11.4.1", @@ -150,16 +151,16 @@ "react-native-fast-pbkdf2": "npm:@onekeyfe/react-native-pbkdf2@3.0.78", "react-native-fs": "npm:@dr.pogodin/react-native-fs@2.34.0", "react-native-gesture-handler": "2.30.0", - "react-native-get-random-values": "npm:@onekeyfe/react-native-get-random-values@3.0.78", + "react-native-get-random-values": "npm:@onekeyfe/react-native-get-random-values@3.0.82", "react-native-image-colors": "^2.5.0", "react-native-image-crop-picker": "0.51.1", "react-native-keyboard-controller": "1.20.7", "react-native-level-fs": "3.0.1", - "react-native-mmkv": "4.1.0", + "react-native-mmkv": "4.2.0", "react-native-modal": "^13.0.1", "react-native-network-info": "npm:@onekeyfe/react-native-network-info@3.0.78", "react-native-network-logger": "2.0.1", - "react-native-nitro-modules": "0.33.2", + "react-native-nitro-modules": "0.35.2", "react-native-pager-view": "npm:@onekeyfe/react-native-pager-view@3.0.78", "react-native-passkeys": "0.3.3", "react-native-permissions": "5.4.4", @@ -179,7 +180,7 @@ "react-native-webview": "13.15.0", "react-native-webview-cleaner": "npm:@onekeyfe/react-native-webview-cleaner@1.0.0", "react-native-worklets": "0.7.1", - "react-native-zip-archive": "npm:@onekeyfe/react-native-zip-archive@3.0.78", + "react-native-zip-archive": "npm:@onekeyfe/react-native-zip-archive@3.0.82", "readable-stream": "^3.6.0", "realm": "20.2.0", "realm-flipper-plugin-device": "^1.1.0", diff --git a/development/envExposedToClient.js b/development/envExposedToClient.js index fc7288e8b8b7..a63656bd8575 100644 --- a/development/envExposedToClient.js +++ b/development/envExposedToClient.js @@ -49,6 +49,11 @@ function buildEnvExposedToClientDangerously({ platform }) { if (platform === developmentConsts.platforms.app) { transformInlineEnvironmentVariables.push('JPUSH_KEY'); transformInlineEnvironmentVariables.push('JPUSH_CHANNEL'); + // Onramper Headless SDK production pair (publishable key + client id, + // NOT the backend partner secret) — consumed by + // shared/src/modules3rdParty/onramper/realClient.native.ts. + transformInlineEnvironmentVariables.push('ONRAMPER_CLIENT_ID'); + transformInlineEnvironmentVariables.push('ONRAMPER_API_KEY'); } return transformInlineEnvironmentVariables; diff --git a/development/spellCheckerSkipWords.txt b/development/spellCheckerSkipWords.txt index 2236ea3de2a4..e1f7440024ce 100644 --- a/development/spellCheckerSkipWords.txt +++ b/development/spellCheckerSkipWords.txt @@ -260,6 +260,7 @@ Cmp Codec codespace coinbase +coinbasepay coingecko coinselect cointype @@ -629,6 +630,7 @@ onekeyhq onekeyid onopen onramper +onramps Ons onsuccess onupgradeneeded @@ -650,6 +652,7 @@ passcode passpharse passphrase pathname +paybis payin payout Pbkdf2 diff --git a/package.json b/package.json index c6a15bb6e7f9..822f40307666 100644 --- a/package.json +++ b/package.json @@ -241,7 +241,7 @@ "react-native": "0.81.5", "react-native-confirmation-code-field": "^7.4.0", "react-native-draggable-flatlist": "4.0.3", - "react-native-get-random-values": "npm:@onekeyfe/react-native-get-random-values@3.0.78", + "react-native-get-random-values": "npm:@onekeyfe/react-native-get-random-values@3.0.82", "react-native-reanimated": "4.2.1", "react-native-screens": "4.23.0", "react-native-web": "0.21.2", @@ -473,7 +473,7 @@ "react-native-reanimated": "4.2.1", "react-native-worklets": "0.7.1", "react-native-screens": "4.23.0", - "react-native-get-random-values": "npm:@onekeyfe/react-native-get-random-values@3.0.78", + "react-native-get-random-values": "npm:@onekeyfe/react-native-get-random-values@3.0.82", "@onekeyfe/react-native-ble-utils": "0.1.6", "@isaacs/brace-expansion": "5.0.1", "fast-xml-parser": "4.5.4", diff --git a/packages/kit-bg/src/services/ServiceFiatCrypto.ts b/packages/kit-bg/src/services/ServiceFiatCrypto.ts index d806b6fcb06e..03f37735b2e0 100644 --- a/packages/kit-bg/src/services/ServiceFiatCrypto.ts +++ b/packages/kit-bg/src/services/ServiceFiatCrypto.ts @@ -15,6 +15,7 @@ import type { IGenerateWidgetUrlResponse, IGenerateWidgetUrlWithAccountId, IGetTokensListParams, + IOnramperSessionResponse, } from '@onekeyhq/shared/types/fiatCrypto'; import ServiceBase from './ServiceBase'; @@ -131,6 +132,42 @@ class ServiceFiatCrypto extends ServiceBase { const isSupported = Boolean(res.url && res.build); return isSupported; } + + // Mint an Onramper Headless SDK session via the OneKey backend. The backend + // SigV2-signs and forwards to Onramper partners/v2 client-sessions, returning + // the { sessionId, sessionToken } pair the SDK consumes. + @backgroundMethod() + public async fetchOnramperSession(): Promise { + // The backend restricts sessions to exactly this scope pair anyway. + const scope = ['quotes:read', 'checkout:write']; + const client = await this.getClient(EServiceEndpointEnum.Wallet); + const resp = await client.post<{ data: IOnramperSessionResponse }>( + '/wallet/v1/fiat-pay/onramper-session', + { scope }, + ); + return resp.data.data; + } + + // Resolve a buy-list token for the native Headless SDK path. Reads the + // (memoized) fiat-pay list; the caller checks `headlessSupported` / + // `onramperNetworkCode` on the result. Used by direct-buy entry points + // that don't already carry the list token. + @backgroundMethod() + public async getHeadlessBuyToken(params: { + networkId: string; + tokenAddress: string; + accountId?: string; + }): Promise { + const { networkId, tokenAddress, accountId } = params; + const tokens = await this.getTokensList({ + networkId, + type: 'buy', + accountId, + }); + return tokens.find( + (o) => o.address.toLowerCase() === tokenAddress.toLowerCase(), + ); + } } export default ServiceFiatCrypto; diff --git a/packages/kit/assets/onramp_coinbasepay.png b/packages/kit/assets/onramp_coinbasepay.png new file mode 100644 index 000000000000..00a20fee62c4 Binary files /dev/null and b/packages/kit/assets/onramp_coinbasepay.png differ diff --git a/packages/kit/assets/onramp_paybis.png b/packages/kit/assets/onramp_paybis.png new file mode 100644 index 000000000000..1ebe1ad7b4c2 Binary files /dev/null and b/packages/kit/assets/onramp_paybis.png differ diff --git a/packages/kit/src/components/NativeAmountKeypad/index.tsx b/packages/kit/src/components/NativeAmountKeypad/index.tsx new file mode 100644 index 000000000000..4bfdcc209bc9 --- /dev/null +++ b/packages/kit/src/components/NativeAmountKeypad/index.tsx @@ -0,0 +1,99 @@ +import { memo } from 'react'; + +import { + Haptics, + Icon, + ImpactFeedbackStyle, + SizableText, + Stack, + XStack, + YStack, +} from '@onekeyhq/components'; +import platformEnv from '@onekeyhq/shared/src/platformEnv'; + +// Numeric keypad grid, forked from the Perps deposit keypad +// (PerpsNativeAmountKeypad) WITHOUT its baked-in CTA, so the buy action zone can +// render separately below it. Presentational only — all state lives in the parent. +const AMOUNT_KEYPAD_ROWS = [ + ['1', '2', '3'], + ['4', '5', '6'], + ['7', '8', '9'], + ['.', '0', 'backspace'], +] as const; + +const KEY_INTERACTIVE_STYLE = { opacity: 1, bg: '$bgStrong' } as const; + +// The standard keypad editing rules, as a pure function so consumers can use +// a dep-free functional state updater (keeping the memoized keypad below from +// re-rendering on every keystroke): backspace deletes one char, a lone '.' +// seeds '0.', a second '.' is ignored, and a leading '0' is replaced by the +// typed digit. +export function applyAmountKeypadKey(prev: string, key: string): string { + if (key === 'backspace') { + return prev.slice(0, -1); + } + if (key === '.') { + if (prev.includes('.')) { + return prev; + } + return prev ? `${prev}.` : '0.'; + } + return prev === '0' ? key : `${prev}${key}`; +} + +type IProps = { + onKeyPress: (key: string) => void; + onBackspaceLongPress: () => void; +}; + +function BasicNativeAmountKeypad({ onKeyPress, onBackspaceLongPress }: IProps) { + return ( + + {AMOUNT_KEYPAD_ROWS.map((row) => ( + + {row.map((item) => ( + { + // Selection-grade tick per key (no-op off-native); the + // clear-all long-press below gets a firmer impact instead. + Haptics.selection(); + onKeyPress(item); + }} + // Native-only: tamagui's web build folds onLongPress into the + // same composed click handler as onPress, so on web/desktop a + // single backspace click would also fire the clear-all. + onLongPress={ + item === 'backspace' && platformEnv.isNative + ? () => { + Haptics.impact(ImpactFeedbackStyle.Medium); + onBackspaceLongPress(); + } + : undefined + } + > + {item === 'backspace' ? ( + + ) : ( + + {item} + + )} + + ))} + + ))} + + ); +} + +// The 12 pressable cells sit on the typing hot path — memoized so they render +// once per mount as long as the parent passes dep-free handlers. +export const NativeAmountKeypad = memo(BasicNativeAmountKeypad); diff --git a/packages/kit/src/views/AssetDetails/pages/TokenDetails/ActionBuy.tsx b/packages/kit/src/views/AssetDetails/pages/TokenDetails/ActionBuy.tsx index a81828d5a444..aaa74a7a3bdc 100644 --- a/packages/kit/src/views/AssetDetails/pages/TokenDetails/ActionBuy.tsx +++ b/packages/kit/src/views/AssetDetails/pages/TokenDetails/ActionBuy.tsx @@ -14,6 +14,7 @@ import { import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; import { usePromiseResult } from '@onekeyhq/kit/src/hooks/usePromiseResult'; import { useUserWalletProfile } from '@onekeyhq/kit/src/hooks/useUserWalletProfile'; +import { tryOpenHeadlessBuy } from '@onekeyhq/kit/src/views/FiatCrypto/utils/openFiatCryptoOrHeadless'; import { ActionItem } from '@onekeyhq/kit/src/views/Home/components/WalletActions/RawActions'; import { WALLET_TYPE_WATCHING } from '@onekeyhq/shared/src/consts/dbConsts'; import { ETranslations } from '@onekeyhq/shared/src/locale'; @@ -83,6 +84,15 @@ export async function openFiatCryptoWidget({ }); } + // Native Headless buy intercept — after the analytics above so the buy + // funnel stays intact; falls through to the web widget when unavailable. + if ( + type === 'buy' && + (await tryOpenHeadlessBuy({ networkId, tokenAddress, accountId })) + ) { + return; + } + const { url } = await backgroundApiProxy.serviceFiatCrypto.generateWidgetUrl({ networkId, tokenAddress, diff --git a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/HeadlessBuyGallery.tsx b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/HeadlessBuyGallery.tsx new file mode 100644 index 000000000000..1c5cc58c39a9 --- /dev/null +++ b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/HeadlessBuyGallery.tsx @@ -0,0 +1,55 @@ +import { Button, Page, SizableText, YStack } from '@onekeyhq/components'; +import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; +import { EModalRoutes } from '@onekeyhq/shared/src/routes'; +import { EModalFiatCryptoRoutes } from '@onekeyhq/shared/src/routes/fiatCrypto'; +import type { IFiatCryptoToken } from '@onekeyhq/shared/types/fiatCrypto'; + +// Dev-only launcher for previewing the Onramper Headless buy page on the +// Simulator with the mock client. Not part of any production flow. +const PREVIEW_TOKEN: IFiatCryptoToken = { + address: '', + name: 'Ethereum', + symbol: 'ETH', + networkId: 'evm--1', + icon: '', + headlessSupported: true, + onramperNetworkCode: 'ethereum', +}; + +function HeadlessBuyGallery() { + const navigation = useAppNavigation(); + return ( + + + + + + Opens the native Headless buy page backed by the mock client. Type + an amount to see the debounced quote + button swap; enter 111111 to + force the web-fallback (S5) and 222222 to force a retryable error + (S4). + + + + + + ); +} + +export default HeadlessBuyGallery; diff --git a/packages/kit/src/views/Developer/pages/Gallery/index.tsx b/packages/kit/src/views/Developer/pages/Gallery/index.tsx index fa40d9790fb5..21d46889dd9d 100644 --- a/packages/kit/src/views/Developer/pages/Gallery/index.tsx +++ b/packages/kit/src/views/Developer/pages/Gallery/index.tsx @@ -310,6 +310,11 @@ const PlaygroundGallery = LazyLoadPage( import('@onekeyhq/kit/src/views/Developer/pages/Gallery/Components/stories/playground/index'), ); +const HeadlessBuyGallery = LazyLoadPage( + () => + import('@onekeyhq/kit/src/views/Developer/pages/Gallery/Components/stories/HeadlessBuyGallery'), +); + export const galleryScreenList: { name: EGalleryRoutes; component: ComponentType; @@ -555,4 +560,8 @@ export const galleryScreenList: { name: EGalleryRoutes.ComponentStorage, component: StorageGallery, }, + { + name: EGalleryRoutes.ComponentHeadlessBuy, + component: HeadlessBuyGallery, + }, ]; diff --git a/packages/kit/src/views/FiatCrypto/components/Headless/BuyActionZone.tsx b/packages/kit/src/views/FiatCrypto/components/Headless/BuyActionZone.tsx new file mode 100644 index 000000000000..e28062d64f55 --- /dev/null +++ b/packages/kit/src/views/FiatCrypto/components/Headless/BuyActionZone.tsx @@ -0,0 +1,150 @@ +import type { ReactNode } from 'react'; + +import { + Button, + SizableText, + Spinner, + Stack, + YStack, +} from '@onekeyhq/components'; + +import { EBuyActionState } from './types'; + +// The native block's RN-side layout height is EXACTLY 56 (device-measured +// 2026-07-17, coinbasepay): the SwiftUI consent copy under the button draws +// in OVERFLOW below the view's bounds and occupies no RN layout space at all. +// So the reserve only needs to even out our 46pt placeholder/retry buttons +// against the 56pt native button; content is TOP-anchored so the button's +// top edge never moves across states (the zone's bottom edge is pinned to +// the page bottom, so growth would extend upward — never bottom-align here). +const ZONE_MIN_HEIGHT = 56; + +// Mock-preview only: the real SDK button renders its own consent sentence +// (with document links) under the native Buy button; the mock mimics that +// structure so the reserved slot reads the same in the Gallery. +const MOCK_TOS_TEXT = + 'By continuing you agree to the Onramper Terms of Service and Privacy Policy'; + +type IProps = { + state: EBuyActionState; + nativeButton: ReactNode; + isMock: boolean; + errorText?: string; + onMockPay: () => void; + onRetry: () => void; + onWebFallback: () => void; +}; + +// The pay action zone on the review screen: renders exactly one of S1–S5. +// Branches swap INSTANTLY, with no transition — deliberate and settled: every +// animated variant (cross-fade, sequenced fades, measured-height glide, an +// opaque cover over the native button, page-persisted height ratchets) was +// tried on device and read worse than a hard swap, because the native block's +// height is unknowable up front (button + consent copy chosen per quote by +// the routed provider). Do not reintroduce transitions here. +export function BuyActionZone({ + state, + nativeButton, + isMock, + errorText, + onMockPay, + onRetry, + onWebFallback, +}: IProps) { + let content: ReactNode; + + if (state === EBuyActionState.WebFallback) { + // S5 — structural failure: hand over to the web widget. + content = ( + + {errorText ? ( + + {errorText} + + ) : null} + + + ); + } else if (state === EBuyActionState.RetryableError) { + // S4 — retryable error (message rendered in the amount hero slot above). + content = ( + + ); + } else if ( + state === EBuyActionState.Preparing || + state === EBuyActionState.InvalidAmount + ) { + // S1 — quoting (spinner) / invalid amount (plain disabled). + content = ( + + ); + } else { + // S2 / S3 — ready or refreshing (e.g. provider switch). In mock mode we + // render our own button (the mock returns button: null); on device this + // is the SDK native button. + content = ( + + {isMock ? ( + + + + {MOCK_TOS_TEXT} + + + ) : ( + nativeButton + )} + {state === EBuyActionState.Refreshing ? ( + + + + ) : null} + + ); + } + + return ( + // Top-anchored on purpose — see ZONE_MIN_HEIGHT. + {content} + ); +} diff --git a/packages/kit/src/views/FiatCrypto/components/Headless/CheckoutInfoCard.tsx b/packages/kit/src/views/FiatCrypto/components/Headless/CheckoutInfoCard.tsx new file mode 100644 index 000000000000..4fef6079446e --- /dev/null +++ b/packages/kit/src/views/FiatCrypto/components/Headless/CheckoutInfoCard.tsx @@ -0,0 +1,248 @@ +import type { ReactNode } from 'react'; + +import Animated, { + FadeIn, + FadeOut, + LinearTransition, +} from 'react-native-reanimated'; + +import { + Divider, + Icon, + NumberSizeableText, + Popover, + SizableText, + Skeleton, + XStack, + YStack, +} from '@onekeyhq/components'; +import { NetworkAvatar } from '@onekeyhq/kit/src/components/NetworkAvatar'; +import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; + +import { DetailRow, formatPrice } from './DetailRow'; +import { + MOTION_EASE_IN, + MOTION_EASE_IN_OUT, + MOTION_EASE_OUT, + MOTION_EXIT_MS, + MOTION_MICRO_MS, +} from './motionTokens'; +import { ProviderLogo, getProviderDisplayName } from './ProviderLogo'; + +type IProps = { + tokenSymbol: string; + // OneKey network id — renders the network's logo before its name. + networkId?: string; + networkName?: string; + // A quote is in flight. Quote-derived rows render as SKELETONS instead of + // unmounting, so the quote landing fills values in place — zero layout + // change, nothing below the card moves (the pay zone below is hard-swap by + // design and must not be displaced). + isQuoting?: boolean; + // USD market price of 1 token from OneKey's own feed — prices the payout's + // parenthetical. Must NOT come from the Onramper quote: its `rate` is just + // payout/amount echoed back (device-verified), and amount−fees can't see + // the spread the provider bakes into the conversion. + marketPrice?: number; + payout?: number; + networkFee?: number; + transactionFee?: number; + providerName?: string; + // Destination wallet address — always shown when known so the user can + // confirm where the funds land before paying. + address?: string; + // When provided the provider row becomes tappable (provider switching). + onSelectProvider?: () => void; +}; + +// The review breakdown — a flat list of label/value rows with a divider +// between every adjacent pair. Rows render conditionally, so they are +// collected first and the dividers interleaved; each bundle fades in/out on +// appearance. No height animation here by design: the hero container above +// owns the layout transition. +export function CheckoutInfoCard({ + tokenSymbol, + networkId, + networkName, + isQuoting, + marketPrice, + payout, + networkFee, + transactionFee, + providerName, + address, + onSelectProvider, +}: IProps) { + const totalFee = (networkFee ?? 0) + (transactionFee ?? 0); + const hasFee = networkFee !== undefined || transactionFee !== undefined; + // Market value of the estimated receive. ≈ because the market feed and the + // provider quote are two sources sampled at slightly different times; when + // no price is available the parenthetical is hidden entirely. + const payoutFiatValue = + payout !== undefined && marketPrice !== undefined + ? payout * marketPrice + : undefined; + const rows: { key: string; node: ReactNode }[] = []; + rows.push({ + key: 'payout', + node: ( + + {payout !== undefined ? ( + + + {payout} + + {payoutFiatValue !== undefined ? ( + + {`(≈${formatPrice(payoutFiatValue)})`} + + ) : null} + + ) : ( + // Typography-matched with the $bodyMdMedium value it replaces. + + )} + + ), + }); + if (hasFee || isQuoting) { + rows.push({ + key: 'fee', + node: ( + + {!hasFee ? ( + + ) : ( + + + {totalFee} + + + + } + renderContent={ + + + {networkName + ? `Fees consist of the ${networkName} network fee and the transaction fee.` + : 'Fees consist of the network fee and the transaction fee.'} + + {networkFee !== undefined ? ( + + + Network fee + + + {formatPrice(networkFee)} + + + ) : null} + {transactionFee !== undefined ? ( + + + Transaction fee + + + {formatPrice(transactionFee)} + + + ) : null} + + } + /> + )} + + ), + }); + } + if (providerName || isQuoting) { + rows.push({ + key: 'provider', + node: !providerName ? ( + + + + ) : ( + + + + + {getProviderDisplayName(providerName)} + + {onSelectProvider ? ( + + ) : null} + + + ), + }); + } + if (networkName) { + rows.push({ + key: 'network', + node: ( + + + {networkId ? ( + + ) : null} + {networkName} + + + ), + }); + } + if (address) { + rows.push({ + key: 'address', + node: ( + + + {accountUtils.shortenAddress({ address })} + + + ), + }); + } + + return ( + + {rows.map((row, index) => ( + // Each bundle (leading divider + row) fades as one unit; keys keep + // React from remounting surviving rows when a neighbor toggles, and + // the layout prop glides survivors into the gap a neighbor leaves + // (or out of the way of one appearing) instead of snapping. + + {index > 0 ? : null} + {row.node} + + ))} + + ); +} diff --git a/packages/kit/src/views/FiatCrypto/components/Headless/DetailRow.tsx b/packages/kit/src/views/FiatCrypto/components/Headless/DetailRow.tsx new file mode 100644 index 000000000000..39651fb554c1 --- /dev/null +++ b/packages/kit/src/views/FiatCrypto/components/Headless/DetailRow.tsx @@ -0,0 +1,46 @@ +import type { ReactNode } from 'react'; + +import { SizableText, XStack } from '@onekeyhq/components'; +import { numberFormat } from '@onekeyhq/shared/src/utils/numberUtils'; + +// Shared label/value row for the review breakdown and the success page's +// order card — one definition so the two surfaces can't drift. +export function DetailRow({ + label, + children, + py, + onPress, +}: { + label: string; + children: ReactNode; + py?: '$2'; + onPress?: () => void; +}) { + return ( + + + {label} + + {children} + + ); +} + +export function formatPrice(value: number) { + return numberFormat(String(value), { + formatter: 'price', + formatterOptions: { currency: '$' }, + }); +} diff --git a/packages/kit/src/views/FiatCrypto/components/Headless/PresetRow.tsx b/packages/kit/src/views/FiatCrypto/components/Headless/PresetRow.tsx new file mode 100644 index 000000000000..a18c9e28bdb8 --- /dev/null +++ b/packages/kit/src/views/FiatCrypto/components/Headless/PresetRow.tsx @@ -0,0 +1,29 @@ +import { Button, XStack } from '@onekeyhq/components'; + +const PRESETS = [50, 100, 200]; + +type IProps = { + onSelect: (value: string) => void; +}; + +// Quick fiat presets shown directly above the keypad. Tapping one only fills +// the amount (no state change, no highlight). Sized like the preview button +// below the keypad: same `large` height, the row spans the full width. +export function PresetRow({ onSelect }: IProps) { + return ( + + {PRESETS.map((value) => ( + + ))} + + ); +} diff --git a/packages/kit/src/views/FiatCrypto/components/Headless/ProviderLogo.tsx b/packages/kit/src/views/FiatCrypto/components/Headless/ProviderLogo.tsx new file mode 100644 index 000000000000..a71140cae085 --- /dev/null +++ b/packages/kit/src/views/FiatCrypto/components/Headless/ProviderLogo.tsx @@ -0,0 +1,37 @@ +import { Image } from '@onekeyhq/components'; + +// Slug → display metadata for the headless providers. Logos are official +// marks bundled at 60×60 (rendered 20×20, so ~3× density) and cropped round +// in the UI; sources: Coinbase GitHub org avatar / cdn.paybis.com +// android-chrome icon, fetched 2026-07-17. New wave-2 providers land here +// (one asset + one entry); unknown slugs degrade to a capitalized slug with +// no logo. Backend-served provider metadata is the eventual replacement. +const PROVIDER_META: Record = { + coinbasepay: { + name: 'Coinbase Pay', + logo: require('@onekeyhq/kit/assets/onramp_coinbasepay.png'), + }, + coinbase: { + name: 'Coinbase', + logo: require('@onekeyhq/kit/assets/onramp_coinbasepay.png'), + }, + paybis: { + name: 'Paybis', + logo: require('@onekeyhq/kit/assets/onramp_paybis.png'), + }, +}; + +export function getProviderDisplayName(slug: string): string { + return ( + PROVIDER_META[slug.toLowerCase()]?.name ?? + slug.charAt(0).toUpperCase() + slug.slice(1) + ); +} + +export function ProviderLogo({ provider }: { provider?: string }) { + const meta = provider ? PROVIDER_META[provider.toLowerCase()] : undefined; + if (!meta) { + return null; + } + return ; +} diff --git a/packages/kit/src/views/FiatCrypto/components/Headless/motionTokens.ts b/packages/kit/src/views/FiatCrypto/components/Headless/motionTokens.ts new file mode 100644 index 000000000000..a012652c3536 --- /dev/null +++ b/packages/kit/src/views/FiatCrypto/components/Headless/motionTokens.ts @@ -0,0 +1,17 @@ +import { Easing } from 'react-native-reanimated'; + +// Motion hierarchy for the Headless buy flow. Three rules everything follows: +// exits are FASTER than enters (the outgoing block gets out of the way, the +// incoming one arrives into clean space — symmetric fades double-expose at +// the midpoint), enters DECELERATE (ease-out) while exits ACCELERATE +// (ease-in), and micro changes (row fades, error text) stay subordinate to +// the mode-level motion. +export const MOTION_ENTER_MS = 175; +export const MOTION_EXIT_MS = 100; +export const MOTION_MICRO_MS = 120; + +// Reanimated's Easing members are worklets — .easing() rejects plain JS +// closures in dev builds, so always use these shared instances. +export const MOTION_EASE_OUT = Easing.out(Easing.quad); +export const MOTION_EASE_IN = Easing.in(Easing.quad); +export const MOTION_EASE_IN_OUT = Easing.inOut(Easing.quad); diff --git a/packages/kit/src/views/FiatCrypto/components/Headless/onramperErrorCopy.ts b/packages/kit/src/views/FiatCrypto/components/Headless/onramperErrorCopy.ts new file mode 100644 index 000000000000..b8468effeada --- /dev/null +++ b/packages/kit/src/views/FiatCrypto/components/Headless/onramperErrorCopy.ts @@ -0,0 +1,78 @@ +import platformEnv from '@onekeyhq/shared/src/platformEnv'; + +// Readable copy for every SDK error code that can surface inline on the buy +// page (quote stage and checkout events). The raw SDK messages are technical +// and must never reach the user. Hardcoded English pending the i18n pass, +// same as the rest of the Headless UI. Codes mirror the SDK's +// `OnramperErrorCode` union (1.1.1). +const ERROR_COPY: Record = { + // User-fixable input errors. `quoteUnavailable` (backend 40003) also covers + // below-minimum amounts — Onramper doesn't expose the limits at quote time, + // so the copy points at the amount as the actionable lever. Copy shown in + // the amount hero slot must fit two $bodySm lines (~110 chars) INCLUDING + // the dev-build ` (code)` suffix, or the tail is cut to an ellipsis. + amountOutOfRange: 'Amount is outside the provider limits. Adjust the amount', + quoteUnavailable: + 'No quote for this amount — it may be outside the provider limits. Try a different amount', + // Transient environment errors. + networkError: 'Network error. Check your connection and try again', + timeout: 'Request timed out. Try again', + temporaryFailure: 'Service temporarily unavailable. Try again later', + invalidRequest: 'Unable to process this request right now. Try again later', + decodingError: 'Failed to parse the response. Try again', + // Stale page / intent state. + notInitialized: 'This page has expired. Try again', + initializationFailed: 'Initialization failed. Try again', + invalidState: 'This page has expired. Try again', + invalidStateTransition: 'This page has expired. Try again', + intentInvalidated: 'The quote has expired. Try again', + intentAlreadyConsumed: 'The quote was already used. Get a new quote', + requirementNotSatisfied: 'A verification step is incomplete. Try again', + // Login / session state. + userTokenInvalid: 'Your login has expired. Try again', + userTokenRefreshFailed: 'Your login has expired. Try again', + sessionExpirationHandlerFailed: 'The session has expired. Try again', + oidcFlowCancelled: 'Identity verification canceled', + oidcFlowFailed: 'Identity verification failed. Try again', + oidcTokenExchangeFailed: 'Identity verification failed. Try again', + // Payment surface. + webviewLoadFailed: 'Unable to open the payment page. Try again', + deepLinkFailed: 'Unable to open the payment page. Try again', + // Device security (retry copy; genuinely blocked devices surface structural + // codes and go to the web fallback instead). + securityStorageFailed: 'Device security check failed. Try again', + securityTrustFailed: 'Device security check failed. Try again', +}; + +const DEFAULT_ERROR_COPY = + 'Unable to complete the purchase right now. Try again later'; + +function toAmountText(value: unknown): string | undefined { + const n = Number(value); + return Number.isFinite(n) && n > 0 ? String(n) : undefined; +} + +export function getOnramperErrorMessage(err: { + code?: string; + message?: string; + info?: Record; +}): string { + let copy = (err?.code && ERROR_COPY[err.code]) || DEFAULT_ERROR_COPY; + // `amountOutOfRange` is validated locally by the SDK and its `info` carries + // the provider's min/max in the source fiat (per the official docs) — show + // the real bounds instead of the generic "adjust the amount" copy. The + // thrown-rejection path loses `info` at the Nitro bridge, so this upgrade + // only fires on the structured `failed`-event path. + const min = toAmountText(err?.info?.min); + const max = toAmountText(err?.info?.max); + if (min !== undefined && max !== undefined) { + copy = `Enter an amount between $${min} and $${max}`; + } else if (min !== undefined) { + copy = `The minimum purchase is $${min}. Adjust the amount`; + } else if (max !== undefined) { + copy = `The maximum purchase is $${max}. Adjust the amount`; + } + // Dev builds append the raw code so on-device QA screenshots map back to the + // SDK error without a log capture. + return platformEnv.isDev && err?.code ? `${copy} (${err.code})` : copy; +} diff --git a/packages/kit/src/views/FiatCrypto/components/Headless/types.ts b/packages/kit/src/views/FiatCrypto/components/Headless/types.ts new file mode 100644 index 000000000000..c56d5354cbf0 --- /dev/null +++ b/packages/kit/src/views/FiatCrypto/components/Headless/types.ts @@ -0,0 +1,9 @@ +// The single enum that drives the pluggable buy action zone (S1–S5 in the UX spec). +export enum EBuyActionState { + Preparing = 'preparing', // S1: session initializing / first quote loading + InvalidAmount = 'invalidAmount', // disabled, waiting for a valid amount + Ready = 'ready', // S2: native (or mock) button ready + Refreshing = 'refreshing', // S3: re-quoting, old button under a loading mask + RetryableError = 'retryableError', // S4: inline error + retry + WebFallback = 'webFallback', // S5: structural failure → web widget +} diff --git a/packages/kit/src/views/FiatCrypto/components/Headless/useOnramperCheckout.ts b/packages/kit/src/views/FiatCrypto/components/Headless/useOnramperCheckout.ts new file mode 100644 index 000000000000..9503bbc35bcc --- /dev/null +++ b/packages/kit/src/views/FiatCrypto/components/Headless/useOnramperCheckout.ts @@ -0,0 +1,382 @@ +import type { ReactNode } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; +import { + createOnramperClient, + getOnramperConfig, + isStructuralOnramperError, +} from '@onekeyhq/shared/src/modules3rdParty/onramper'; +import type { + IOnramperButtonStyle, + IOnramperClient, + IOnramperEvent, + IOnramperQuote, +} from '@onekeyhq/shared/src/modules3rdParty/onramper'; + +import { getOnramperErrorMessage } from './onramperErrorCopy'; +import { EBuyActionState } from './types'; + +const DEBOUNCE_MS = 400; + +type IParams = { + amount: number; // parsed fiat amount (0 when empty/invalid) + isAmountValid: boolean; + source: string; // fiat code, e.g. 'usd' + destination: string; // crypto code, e.g. 'sol' + network: string; // onramper network code + address: string | undefined; + // ISO country code (lowercase); omit to let Onramper geo-detect by IP. + country?: string; + // Pin routing to these provider slugs; omit for Onramper's best choice. + // Must be reference-stable (useMemo) — it is a debounce-effect dependency. + onlyOnramps?: string[]; + buttonStyle: IOnramperButtonStyle; + onCompleted: (event: IOnramperEvent) => void; +}; + +type IResult = { + actionState: EBuyActionState; + nativeButton: ReactNode; + quote: IOnramperQuote | undefined; + isMock: boolean; + errorMessage: string | undefined; + // SDK error code behind errorMessage — lets the page route amount-level + // failures back to the input screen instead of a generic retry. + errorCode: string | undefined; + payMock: () => void; + retry: () => void; + // Clears the stored OnramperID login (OIDC tokens); the next checkout + // re-runs email + phone verification. Exposed for the header sign-out + // button. + signOut: () => Promise; +}; + +// Owns the Headless checkout lifecycle: session mint + client init, the debounced +// getCheckoutRequirements loop, the no-flicker button swap, event wiring, and the +// funnel analytics. +export function useOnramperCheckout({ + amount, + isAmountValid, + source, + destination, + network, + address, + country, + onlyOnramps, + buttonStyle, + onCompleted, +}: IParams): IResult { + const [actionState, setActionState] = useState( + EBuyActionState.Preparing, + ); + const [nativeButton, setNativeButton] = useState(null); + const [quote, setQuote] = useState(undefined); + const [errorMessage, setErrorMessage] = useState( + undefined, + ); + const [errorCode, setErrorCode] = useState(undefined); + const [ready, setReady] = useState(false); + const [retryNonce, setRetryNonce] = useState(0); + + const createClient = useCallback( + () => + createOnramperClient({ + // Real config on device (clientId from Onramper); the mock ignores it. + ...getOnramperConfig(), + onSessionExpired: () => + backgroundApiProxy.serviceFiatCrypto.fetchOnramperSession(), + }), + [], + ); + // Lazily create a single client for the lifetime of the page. + const clientRef = useRef(null); + if (clientRef.current === null) { + clientRef.current = createClient(); + } + const isMock = clientRef.current?.isMock ?? false; + + const reqSeqRef = useRef(0); + const hasButtonRef = useRef(false); + const quoteLoggedRef = useRef(false); + + // Reference-stable logging context so the (dep-free) event callbacks can read + // the current token without re-subscribing. + const logCtxRef = useRef({ + networkId: network, + tokenSymbol: destination.toUpperCase(), + }); + logCtxRef.current = { + networkId: network, + tokenSymbol: destination.toUpperCase(), + }; + + // Keep callbacks fresh without re-subscribing the event listeners. + const onCompletedRef = useRef(onCompleted); + onCompletedRef.current = onCompleted; + + const goWebFallback = useCallback((code?: string) => { + defaultLogger.fiatCrypto.onramper.webFallbackShown({ + ...logCtxRef.current, + errorCode: code, + }); + setActionState(EBuyActionState.WebFallback); + }, []); + + const handleFailed = useCallback( + (event: IOnramperEvent) => { + defaultLogger.fiatCrypto.onramper.checkoutFailed({ + ...logCtxRef.current, + errorCode: event.errorCode, + checkoutId: event.checkoutId, + }); + if ( + isStructuralOnramperError({ + code: event.errorCode, + message: event.message, + }) + ) { + goWebFallback(event.errorCode); + } else { + setErrorMessage( + getOnramperErrorMessage({ + code: event.errorCode, + message: event.message, + info: event.info, + }), + ); + setErrorCode(event.errorCode); + setActionState(EBuyActionState.RetryableError); + } + }, + [goWebFallback], + ); + const handleFailedRef = useRef(handleFailed); + handleFailedRef.current = handleFailed; + + // Init: mint session (skipped for the mock) → initialize → wire events. + useEffect(() => { + // Recreate when a previous cleanup destroyed the client: Fast Refresh (and + // StrictMode) re-run this effect with refs preserved, and every native + // call on the disposed instance throws ("NativeState is null"). + if (!clientRef.current) { + clientRef.current = createClient(); + } + const client = clientRef.current; + setReady(false); + let cancelled = false; + const removeListeners = [ + client.addEventListener('completed', (event) => { + defaultLogger.fiatCrypto.onramper.checkoutCompleted({ + ...logCtxRef.current, + checkoutId: event.checkoutId, + }); + onCompletedRef.current(event); + }), + client.addEventListener('failed', (event) => + handleFailedRef.current(event), + ), + ]; + void (async () => { + try { + const session = client.isMock + ? { sessionId: 'mock', sessionToken: 'mock' } + : await backgroundApiProxy.serviceFiatCrypto.fetchOnramperSession(); + if (cancelled) { + return; + } + await client.initialize(session); + if (!cancelled) { + setReady(true); + } + } catch { + if (!cancelled) { + goWebFallback('sessionMintFailed'); + } + } + })(); + return () => { + cancelled = true; + removeListeners.forEach((remove) => remove()); + // The disposed instance must not be reused — see the recreate guard above. + clientRef.current = null; + // Defer the native teardown out of the unmount/navigation-transition + // window: dispose() cancels SDK tasks and releases the @MainActor client + // from the JS thread, and doing that while the modal dismissal is + // animating has produced intermittent hard freezes on close. By 400ms + // the transition is over. + setTimeout(() => { + try { + client.destroy(); + } catch { + // Best-effort teardown; the instance is already unreachable. + } + }, 400); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Debounced checkout loop: re-quote on amount/token change; keep the old button + // mounted under a mask (S3) and swap in a single commit; drop stale responses. + useEffect(() => { + if (!ready) { + setActionState(EBuyActionState.Preparing); + return undefined; + } + if (!isAmountValid || amount <= 0) { + setActionState(EBuyActionState.InvalidAmount); + return undefined; + } + // Never quote a real checkout against an empty destination address: a + // completed payment would have nowhere to send the crypto. The entry gate + // guarantees an accountId, so the async address lookup lands shortly and + // re-fires this effect (address is a dep); until then hold Preparing. + // The mock stays exempt — the Simulator/Gallery preview has no account. + if (!isMock && !address) { + setActionState(EBuyActionState.Preparing); + return undefined; + } + let cancelled = false; + const seq = reqSeqRef.current + 1; + reqSeqRef.current = seq; + setActionState( + hasButtonRef.current + ? EBuyActionState.Refreshing + : EBuyActionState.Preparing, + ); + setErrorMessage(undefined); + setErrorCode(undefined); + const timer = setTimeout(() => { + const client = clientRef.current; + if (!client) { + return; + } + void (async () => { + try { + const result = await client.getCheckoutRequirements( + { + source, + destination, + amount, + type: 'buy', + paymentMethod: 'applepay', + ...(country ? { country } : {}), + ...(onlyOnramps?.length ? { onlyOnramps } : {}), + wallet: { network, address: address ?? '' }, + }, + buttonStyle, + ); + if (cancelled || seq !== reqSeqRef.current) { + return; + } + setQuote(result.quote); + setNativeButton(result.button); + hasButtonRef.current = true; + if (!quoteLoggedRef.current) { + quoteLoggedRef.current = true; + defaultLogger.fiatCrypto.onramper.quoteReceived({ + ...logCtxRef.current, + amount, + quoteId: result.quote.quoteId, + }); + } + setActionState(EBuyActionState.Ready); + } catch (error) { + if (cancelled || seq !== reqSeqRef.current) { + return; + } + const err = error as { + code?: string; + message?: string; + info?: Record; + }; + // Same classification policy as the SDK's `failed` event — + // structural → web fallback, everything else retryable. + handleFailedRef.current({ + errorCode: err?.code, + message: err?.message, + info: err?.info, + }); + } + })(); + }, DEBOUNCE_MS); + return () => { + cancelled = true; + clearTimeout(timer); + }; + }, [ + ready, + amount, + isAmountValid, + isMock, + source, + destination, + network, + address, + country, + onlyOnramps, + buttonStyle, + retryNonce, + goWebFallback, + ]); + + const payMock = useCallback(() => { + defaultLogger.fiatCrypto.onramper.checkoutCompleted({ + ...logCtxRef.current, + checkoutId: 'mock-checkout', + }); + onCompletedRef.current({ checkoutId: 'mock-checkout' }); + }, []); + + const retry = useCallback(() => { + // Drop the previous quote's data and button up front: the button handle is + // single-consume (remounting the old element renders blank) and a fast + // exit-review → re-enter must show loading states, not the prior quote's + // figures, until the fresh quote lands. + setQuote(undefined); + setNativeButton(null); + hasButtonRef.current = false; + // Enter Preparing in the SAME commit: the async reset()→re-quote path + // updates actionState a few frames later, and in that gap "quote cleared + // but still Ready" the estimate line would flash 0 before its skeleton. + setActionState(EBuyActionState.Preparing); + // Defer reset() past the unmount frames. Dispatched synchronously it + // reaches the SDK BEFORE React commits the review unmount (device-traced: + // the idle/ready state events land ~1.5ms before the mode flip), and the + // still-visible SwiftUI button reacts — its consent copy drops and the + // button re-centers downward inside its hosting view — a visible jolt. + // Two frames later the view is off screen and the re-layout is invisible. + setTimeout(() => { + void (async () => { + // Per the SDK docs, a terminal `failed` checkout needs reset() before + // starting another one (returns the state machine to `ready`); + // harmless when the failure never left the quote stage. Official + // example does the same between checkouts. + try { + await clientRef.current?.reset(); + } catch { + // A reset failure must not block the re-quote; the quote loop + // surfaces its own error if the client is genuinely unusable. + } + setRetryNonce((n) => n + 1); + })(); + }, 64); + }, []); + + const signOut = useCallback(async () => { + await clientRef.current?.signOut(); + }, []); + + return { + actionState, + nativeButton, + quote, + isMock, + errorMessage, + errorCode, + payMock, + retry, + signOut, + }; +} diff --git a/packages/kit/src/views/FiatCrypto/components/SellOrBuy/index.tsx b/packages/kit/src/views/FiatCrypto/components/SellOrBuy/index.tsx index e8e593bae0a8..ed712accc916 100644 --- a/packages/kit/src/views/FiatCrypto/components/SellOrBuy/index.tsx +++ b/packages/kit/src/views/FiatCrypto/components/SellOrBuy/index.tsx @@ -6,6 +6,7 @@ import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/background import { useAccountData } from '@onekeyhq/kit/src/hooks/useAccountData'; import { TokenList } from '@onekeyhq/kit/src/views/FiatCrypto/components/TokenList'; import { useGetTokensList } from '@onekeyhq/kit/src/views/FiatCrypto/hooks'; +import { tryOpenHeadlessBuy } from '@onekeyhq/kit/src/views/FiatCrypto/utils/openFiatCryptoOrHeadless'; import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; import networkUtils from '@onekeyhq/shared/src/utils/networkUtils'; @@ -90,6 +91,17 @@ export const SellOrBuyContent = memo( networkID: token.networkId, }); } + if ( + type === 'buy' && + (await tryOpenHeadlessBuy({ + networkId: token.networkId, + tokenAddress: token.address, + accountId: realAccountId, + token, + })) + ) { + return; + } const { url } = await backgroundApiProxy.serviceFiatCrypto.generateWidgetUrl({ networkId: token.networkId, diff --git a/packages/kit/src/views/FiatCrypto/pages/HeadlessBuy/Success.tsx b/packages/kit/src/views/FiatCrypto/pages/HeadlessBuy/Success.tsx new file mode 100644 index 000000000000..c2228e85ce52 --- /dev/null +++ b/packages/kit/src/views/FiatCrypto/pages/HeadlessBuy/Success.tsx @@ -0,0 +1,164 @@ +import { useCallback } from 'react'; + +import { useRoute } from '@react-navigation/core'; + +import { + Icon, + LottieView, + NumberSizeableText, + Page, + SizableText, + Stack, + XStack, + YStack, + useClipboard, +} from '@onekeyhq/components'; +import { NetworkAvatar } from '@onekeyhq/kit/src/components/NetworkAvatar'; +import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; +import type { + EModalFiatCryptoRoutes, + IModalFiatCryptoParamList, +} from '@onekeyhq/shared/src/routes'; +import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; + +import { DetailRow, formatPrice } from '../../components/Headless/DetailRow'; +import { + ProviderLogo, + getProviderDisplayName, +} from '../../components/Headless/ProviderLogo'; + +import type { RouteProp } from '@react-navigation/core'; + +// Post-payment result page. One centered group — the success landing +// animation (the same Lottie the Swap/DeFi results use), title/description, +// and the order details in a compact card (no dividers). Reached via +// CommonActions.reset — the buy flow behind it no longer exists in the stack, +// so back gestures can only dismiss the whole modal. Copy stays honest about +// the asynchronous settlement: the order is SUBMITTED; delivery happens +// server-side later. +function HeadlessBuySuccessPage() { + const navigation = useAppNavigation(); + const route = + useRoute< + RouteProp< + IModalFiatCryptoParamList, + EModalFiatCryptoRoutes.HeadlessBuySuccess + > + >(); + const { + fiatAmount, + tokenSymbol, + networkId, + networkName, + payout, + providerName, + address, + checkoutId, + } = route.params; + + const handleDone = useCallback(() => { + navigation.popStack(); + }, [navigation]); + + // Full id to the clipboard (the row shows it shortened) — it is the only + // handle support/Onramper can look an order up by. + const { copyText } = useClipboard(); + const handleCopyOrderId = useCallback(() => { + if (checkoutId) { + copyText(checkoutId); + } + }, [checkoutId, copyText]); + + return ( + + null} /> + + + + {/* Same landing animation the Swap/DeFi success results use. */} + + + {/* Deliberately NOT "Payment successful": the SDK's `completed` + event only means the order was handed to the provider — the + charge and delivery settle asynchronously on their side and + can still fail (observed in production, 2026-08-13). */} + + Order submitted + + + {tokenSymbol} will be sent to your address once the payment is + processed + + + + + + {formatPrice(fiatAmount)} + + + {payout !== undefined ? ( + + + {payout} + + + ) : null} + {providerName ? ( + + + + + {getProviderDisplayName(providerName)} + + + + ) : null} + + + + + {networkName ?? ''} + + + + {address ? ( + + + {accountUtils.shortenAddress({ address })} + + + ) : null} + {checkoutId ? ( + + + + {accountUtils.shortenAddress({ address: checkoutId })} + + + + + ) : null} + + + + + + + ); +} + +export default HeadlessBuySuccessPage; diff --git a/packages/kit/src/views/FiatCrypto/pages/HeadlessBuy/TokenSelector.tsx b/packages/kit/src/views/FiatCrypto/pages/HeadlessBuy/TokenSelector.tsx new file mode 100644 index 000000000000..87c03b92d6b9 --- /dev/null +++ b/packages/kit/src/views/FiatCrypto/pages/HeadlessBuy/TokenSelector.tsx @@ -0,0 +1,90 @@ +import { useCallback, useMemo } from 'react'; + +import { useRoute } from '@react-navigation/core'; + +import { Page } from '@onekeyhq/components'; +import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; +import type { + EModalFiatCryptoRoutes, + IModalFiatCryptoParamList, +} from '@onekeyhq/shared/src/routes'; +import type { IFiatCryptoToken } from '@onekeyhq/shared/types/fiatCrypto'; + +import { NetworkContainer } from '../../components/NetworkContainer'; +import { TokenDataContainer } from '../../components/TokenDataContainer'; +import { TokenList } from '../../components/TokenList'; +import { useGetTokensList } from '../../hooks'; + +import type { RouteProp } from '@react-navigation/core'; + +// Destination-crypto picker for the Headless buy screen: the buy token list of +// the entry network, narrowed to tokens the native Apple Pay flow can actually +// purchase. Same-network only, so the receiving account never changes chain. +function HeadlessBuyTokenSelectorPage() { + const route = + useRoute< + RouteProp< + IModalFiatCryptoParamList, + EModalFiatCryptoRoutes.HeadlessBuyTokenSelector + > + >(); + const { networkId, accountId, onSelected } = route.params; + const navigation = useAppNavigation(); + + const { result: tokens, isLoading } = useGetTokensList({ + networkId, + accountId, + type: 'buy', + }); + + // Same eligibility as tryOpenHeadlessBuy's gate: a token missing the + // server-delivered network slug can never quote, so it must not be offered. + const items = useMemo( + () => tokens.filter((o) => o.headlessSupported && o.onramperNetworkCode), + [tokens], + ); + + const networkIds = useMemo( + () => Array.from(new Set(items.map((o) => o.networkId))), + [items], + ); + + const handlePress = useCallback( + ({ + token, + realAccountId, + }: { + token: IFiatCryptoToken; + realAccountId?: string; + }) => { + onSelected({ token, realAccountId }); + navigation.pop(); + }, + [onSelected, navigation], + ); + + return ( + + + + + + + + + + + ); +} + +export default HeadlessBuyTokenSelectorPage; diff --git a/packages/kit/src/views/FiatCrypto/pages/HeadlessBuy/index.tsx b/packages/kit/src/views/FiatCrypto/pages/HeadlessBuy/index.tsx new file mode 100644 index 000000000000..23f9b27dbaa7 --- /dev/null +++ b/packages/kit/src/views/FiatCrypto/pages/HeadlessBuy/index.tsx @@ -0,0 +1,626 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { CommonActions, useNavigation, useRoute } from '@react-navigation/core'; +import Animated, { + FadeIn, + FadeOut, + LinearTransition, + useAnimatedStyle, + withTiming, +} from 'react-native-reanimated'; + +import { + ActionList, + Button, + HeaderIconButton, + Page, + SizableText, + Stack, + Toast, + YStack, + usePreventRemove, +} from '@onekeyhq/components'; +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import { + NativeAmountKeypad, + applyAmountKeypadKey, +} from '@onekeyhq/kit/src/components/NativeAmountKeypad'; +import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation'; +import { usePromiseResult } from '@onekeyhq/kit/src/hooks/usePromiseResult'; +import { validateAmountInput } from '@onekeyhq/kit/src/utils/validateAmountInput'; +import { SendAutoSizeAmountInput } from '@onekeyhq/kit/src/views/Send/components/SendAutoSizeAmountInput'; +import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; +import type { + IOnramperEvent, + IOnramperQuote, +} from '@onekeyhq/shared/src/modules3rdParty/onramper'; +import { EModalFiatCryptoRoutes } from '@onekeyhq/shared/src/routes'; +import type { IModalFiatCryptoParamList } from '@onekeyhq/shared/src/routes'; +import { openFiatCryptoUrl } from '@onekeyhq/shared/src/utils/openUrlUtils'; + +import { BuyActionZone } from '../../components/Headless/BuyActionZone'; +import { CheckoutInfoCard } from '../../components/Headless/CheckoutInfoCard'; +import { + MOTION_EASE_IN, + MOTION_EASE_IN_OUT, + MOTION_EASE_OUT, + MOTION_ENTER_MS, + MOTION_EXIT_MS, + MOTION_MICRO_MS, +} from '../../components/Headless/motionTokens'; +import { PresetRow } from '../../components/Headless/PresetRow'; +import { getProviderDisplayName } from '../../components/Headless/ProviderLogo'; +import { EBuyActionState } from '../../components/Headless/types'; +import { useOnramperCheckout } from '../../components/Headless/useOnramperCheckout'; + +import type { RouteProp } from '@react-navigation/core'; + +// Pure black pill in both themes (deliberate design choice, not theme-driven); +// borderRadius = half of the native button's 56pt min-height → fully rounded. +const HEADLESS_BUY_BUTTON_STYLE = { + backgroundColor: '#000000', + foregroundColor: '#FFFFFF', + borderRadius: 28, +} as const; + +// Two-state screen: the amount block (big fiat figure + ≈crypto estimate) +// persists across both states, centered in the space the lower half leaves; +// 'input' shows a presets/preview-CTA slot + keypad (the slot morphs into the +// preview button once any amount exists), 'review' swaps that lower half for +// the order breakdown + pay button. Quoting runs from the first typed digit, +// so the estimate is live on the input screen. +type IBuyMode = 'input' | 'review'; + +// Amount-level quote failures return the user to the input screen (inline +// error under the amount) instead of offering an in-place retry. +const AMOUNT_ERROR_CODES = new Set(['amountOutOfRange', 'quoteUnavailable']); + +// Mode-swap animation: the leaving lower block fades out fast (accelerating, +// overlaid at its last position), the entering one fades in slower +// (decelerating) into the cleared space, and the amount block's layout +// transition glides it to its new centered position over the enter duration — +// reanimated coordinates all three, which moti's AnimatePresence did not (the +// hero snapped). Durations/easings come from the shared motion tokens. + +function HeadlessBuyPage() { + const route = + useRoute< + RouteProp + >(); + const { networkId, accountId, tokenAddress, token } = route.params; + const navigation = useAppNavigation(); + const reactNavigation = useNavigation(); + + const [amountText, setAmountText] = useState(''); + const [mode, setMode] = useState('input'); + // Hero layout transitions are armed shortly AFTER mount, not at mount: the + // initial load performs multi-pass layout settling (native header height + // lands after the first pass; the auto-size input re-renders after + // measuring its width) and an always-on layout prop animates that settling + // as a visible drift. The timer must also beat the first interaction: a + // preset tap swaps to review in the same commit it sets the amount, and the + // layout prop only animates changes committed AFTER it is attached. + const [layoutAnimReady, setLayoutAnimReady] = useState(false); + useEffect(() => { + // Comfortably past the 1–2 frame settle, well under human reaction time. + const timer = setTimeout(() => setLayoutAnimReady(true), 300); + return () => clearTimeout(timer); + }, []); + // Pinned provider slug; undefined = let Onramper route to the best provider. + const [selectedProvider, setSelectedProvider] = useState( + undefined, + ); + + const handleAmountChange = useCallback((value: string) => { + // USD input → 2 decimal places. + if (!validateAmountInput(value, 2)) { + return; + } + setAmountText(value); + }, []); + + // Dep-free functional updaters: the memoized keypad's cells sit on the + // typing hot path and must not re-render per keystroke. + const handleKeyPress = useCallback((key: string) => { + setAmountText((prev) => { + const next = applyAmountKeypadKey(prev, key); + // USD input → 2 decimal places. + return validateAmountInput(next, 2) ? next : prev; + }); + }, []); + + const handleBackspaceLongPress = useCallback(() => { + setAmountText(''); + }, []); + + const amount = Number(amountText) || 0; + + const enterReview = useCallback(() => { + setMode('review'); + }, []); + + // Preset tap = one-step buy entry: fill the amount AND jump straight to + // review (the quote debounce runs there; the zone shows its quoting state + // meanwhile). + const handlePresetSelect = useCallback( + (value: string) => { + handleAmountChange(value); + setMode('review'); + }, + [handleAmountChange], + ); + + // Set right before an intentional close (completed checkout) so the + // review-mode leave interception below lets that removal through. + const allowLeaveRef = useRef(false); + + // Resolve the route token from the network's buy list — only when the entry + // passed just an address (the list fetch is wasted otherwise). + const { result: buyTokens } = usePromiseResult( + () => + token + ? Promise.resolve(undefined) + : backgroundApiProxy.serviceFiatCrypto.getTokensList({ + networkId, + type: 'buy', + accountId, + }), + [token, networkId, accountId], + ); + const tokenFromList = useMemo( + () => + buyTokens?.find( + (o) => o.address.toLowerCase() === tokenAddress.toLowerCase(), + ), + [buyTokens, tokenAddress], + ); + const activeToken = token ?? tokenFromList; + + // If a navigate() re-targets this mounted screen with different route params + // (a second buy entry while the modal is open), drop the provider pin — its + // coverage was scoped to the previous token. + useEffect(() => { + setSelectedProvider(undefined); + }, [networkId, tokenAddress]); + + const enterLoggedRef = useRef(false); + useEffect(() => { + if (activeToken && !enterLoggedRef.current) { + enterLoggedRef.current = true; + defaultLogger.fiatCrypto.onramper.enterAmountPage({ + networkId, + tokenSymbol: activeToken.symbol, + }); + } + }, [activeToken, networkId]); + + const { result: address } = usePromiseResult(async () => { + if (!accountId) { + return undefined; + } + return backgroundApiProxy.serviceAccount.getAccountAddressForApi({ + networkId, + accountId, + }); + }, [accountId, networkId]); + + // Network display name for the review card's network row. Keyed on the + // ROUTE id, not activeToken (the token is resolved by matching this very + // network/address, so the values are identical — depending on activeToken + // would just re-fire the fetch when the async list resolves). + const { result: activeNetwork } = usePromiseResult( + () => backgroundApiProxy.serviceNetwork.getNetwork({ networkId }), + [networkId], + ); + + // USD market price of the token, from OneKey's own feed — the review card + // prices the payout with it. Deliberately NOT derived from the Onramper + // quote: the SDK's `rate` is exactly payout/amount echoed back (verified on + // device 2026-07-17), so it can never reveal spread or a bad quote. + const { result: marketPrice } = usePromiseResult(async () => { + try { + const detail = + await backgroundApiProxy.serviceMarketV2.fetchMarketTokenDetailByTokenAddress( + // Route params, not activeToken: identical values (the token is + // matched by this address), and this endpoint is un-memoized — an + // activeToken dep would duplicate the HTTP call once the list + // resolves. + tokenAddress, + networkId, + { skipConvertCurrency: true, autoHandleError: false }, + ); + const price = Number(detail?.data?.token?.price); + return Number.isFinite(price) && price > 0 ? price : undefined; + } catch { + // No market price (long-tail token / endpoint hiccup): the card hides + // the parenthetical instead of substituting a different-meaning number. + return undefined; + } + }, [tokenAddress, networkId]); + + // The address actually used for quoting; also shown on the review card so + // the user can confirm the destination before paying. + const effectiveAddress = address; + + // Reference-stable: this is a dependency of the hook's debounce effect. + const onlyOnramps = useMemo( + () => (selectedProvider ? [selectedProvider] : undefined), + [selectedProvider], + ); + + // Latest quote for the completion handler — handleCompleted is a param of + // the checkout hook, so it is defined before the hook returns `quote`; + // the ref is assigned right after the hook call below. + const quoteRef = useRef(undefined); + + const handleCompleted = useCallback( + (event: IOnramperEvent) => { + // Bypass the review leave guard first: the reset below removes this + // screen and fires beforeRemove. Then WIPE the buy flow from the modal + // stack instead of pushing — the native pay button is consumed, so + // back must not be able to reach it; the success page becomes the only + // route in the stack. + allowLeaveRef.current = true; + navigation.dispatch( + CommonActions.reset({ + index: 0, + routes: [ + { + name: EModalFiatCryptoRoutes.HeadlessBuySuccess, + params: { + fiatAmount: amount, + tokenSymbol: activeToken?.symbol ?? '', + networkId: activeToken?.networkId ?? networkId, + networkName: activeNetwork?.name, + payout: quoteRef.current?.payout, + providerName: selectedProvider ?? quoteRef.current?.ramp, + address: effectiveAddress, + checkoutId: event.checkoutId, + }, + }, + ], + }), + ); + }, + [ + navigation, + amount, + activeToken?.symbol, + activeToken?.networkId, + networkId, + activeNetwork?.name, + selectedProvider, + effectiveAddress, + ], + ); + + const openWebFallback = useCallback(async () => { + const { url } = + await backgroundApiProxy.serviceFiatCrypto.generateWidgetUrl({ + networkId, + tokenAddress: activeToken?.address ?? tokenAddress, + accountId, + // This page is buy-only (sell always stays on the web widget). + type: 'buy', + }); + if (url) { + openFiatCryptoUrl(url); + } + }, [networkId, activeToken?.address, tokenAddress, accountId]); + + const { + actionState, + nativeButton, + quote, + isMock, + errorMessage, + errorCode, + payMock, + retry, + signOut, + } = useOnramperCheckout({ + amount, + // Quote from the first digit — the ≈crypto estimate is live while typing. + isAmountValid: amount > 0 && Boolean(activeToken), + source: 'usd', + destination: activeToken?.symbol?.toLowerCase() ?? '', + // Onramper speaks its own network slugs (e.g. 'ethereum'), not OneKey ids. + // The entry gate requires the server-delivered slug (OK-58060), so this + // only misses for direct navigations (e.g. Gallery) — '' fails the quote + // cleanly rather than sending a raw OneKey id as a slug. + network: activeToken?.onramperNetworkCode ?? '', + address: effectiveAddress, + // No `country`: Onramper geo-detects from the device IP, which is also + // what providers enforce at checkout (country == device IP == + // verified-phone country) — staging tests must run behind a US/EU VPN. + onlyOnramps, + buttonStyle: HEADLESS_BUY_BUTTON_STYLE, + onCompleted: handleCompleted, + }); + quoteRef.current = quote; + + // Leaving review abandons the mounted native pay button: the SDK's prepared + // intents are single-mount, so remounting the same element on a later entry + // renders blank. Re-quote on exit so the next entry gets a fresh button. + const exitReview = useCallback(() => { + setMode('input'); + retry(); + }, [retry]); + + // In review, every page-leave intent (header X / back arrow, Android back, + // iOS swipe) returns to the input view instead of closing the modal. + // A screen-level headerLeft override can't do this reliably: on iOS 26 the + // modal close is a navigator-level `unstable_headerLeftItems` bar item that + // takes the slot over `headerLeft`. usePreventRemove (NOT a raw beforeRemove + // preventDefault): it registers the prevented state with the native stack so + // gesture-driven dismissals are blocked up front — cancelling a native + // gesture after the fact leaves native and JS navigation state out of sync + // and has frozen the app on close (intermittent hard-hang, device-observed). + usePreventRemove(mode === 'review', ({ data }) => { + if (allowLeaveRef.current) { + // Completed checkout: let the blocked action (the success-page reset) + // through. + reactNavigation.dispatch(data.action); + return; + } + exitReview(); + }); + + // Amount-level failures are fixed on the input screen, not retried in place. + // The quote error stays visible under the amount (heroError below) until the + // next edit re-quotes. + useEffect(() => { + if ( + mode === 'review' && + actionState === EBuyActionState.RetryableError && + errorCode && + AMOUNT_ERROR_CODES.has(errorCode) + ) { + setMode('input'); + } + }, [mode, actionState, errorCode]); + + const heroError = + actionState === EBuyActionState.RetryableError ? errorMessage : undefined; + + const heroLayout = layoutAnimReady + ? LinearTransition.duration(MOTION_ENTER_MS).easing(MOTION_EASE_IN_OUT) + : undefined; + + // Always-mounted fade for the error line — the slot is fixed-height (two + // $bodySm lines: error copy wraps, and appearance still never shifts the + // hero layout), so only opacity moves; the last message is retained so the + // text doesn't blank out mid-fade. + const lastHeroErrorRef = useRef(undefined); + if (heroError) { + lastHeroErrorRef.current = heroError; + } + const heroErrorStyle = useAnimatedStyle( + () => ({ + opacity: withTiming(heroError ? 1 : 0, { + duration: MOTION_MICRO_MS, + easing: MOTION_EASE_OUT, + }), + }), + [heroError], + ); + + // The ≈crypto line under the amount: live payout from the quote, a skeleton + // while a quote is in flight (avoids showing a stale figure for the newly + // typed amount), and a plain 0 when there is nothing to quote. + const isQuoting = + amount > 0 && + (actionState === EBuyActionState.Preparing || + actionState === EBuyActionState.Refreshing); + const estimateValueProps = useMemo( + () => ({ + // The hook keeps the last successful quote; with the amount cleared that + // figure is stale, so the estimate falls back to 0 immediately. + value: + amount > 0 && quote?.payout !== undefined ? String(quote.payout) : '0', + tokenSymbol: activeToken?.symbol, + formatter: 'balance' as const, + // Regular body type — the estimate is secondary to the amount above it. + size: '$bodyLg' as const, + loading: isQuoting, + }), + [amount, quote?.payout, activeToken?.symbol, isQuoting], + ); + + // Switchable providers: the current quote's ramp plus the SDK's recommended + // alternatives. TODO(onramper): confirm `recommendations` carries provider + // slugs / ask for a dedicated provider-list endpoint for the headless flow. + const providerOptions = useMemo(() => { + const slugs = new Set(); + if (quote?.ramp) { + slugs.add(quote.ramp); + } + (quote?.recommendations ?? []).forEach((slug) => slugs.add(slug)); + if (selectedProvider) { + slugs.add(selectedProvider); + } + return Array.from(slugs); + }, [quote, selectedProvider]); + // Offer switching when there is a real alternative, or a pin to undo. + const canSwitchProvider = + providerOptions.length > 1 || Boolean(selectedProvider); + + const handleSelectProvider = useCallback(() => { + ActionList.show({ + title: 'Select provider', + sections: [ + { + items: [ + { + label: 'Auto (recommended)', + onPress: () => setSelectedProvider(undefined), + }, + ...providerOptions.map((slug) => ({ + label: getProviderDisplayName(slug), + onPress: () => setSelectedProvider(slug), + })), + ], + }, + ], + }); + }, [providerOptions]); + + // Drops the stored OnramperID login so the next checkout re-runs email + + // phone verification. User-facing on all builds: the SDK keeps the OIDC + // login in native storage and offers no in-flow way to switch accounts — + // without this, a user whose OnramperID cannot transact (e.g. KYC country + // vs current region mismatch) is stuck with no way to try another account. + const renderSignOutButton = useCallback( + () => ( + { + await signOut(); + Toast.success({ + title: 'Signed out of OnramperID', + message: 'The next purchase will re-verify email and phone', + }); + }} + /> + ), + [signOut], + ); + + return ( + + {/* In review the close button returns to input via the beforeRemove + interception above. */} + + + + {/* The amount block persists across both modes and re-centers in + whatever vertical space the lower half leaves; the layout + transition animates that movement. */} + {/* The layout transition lives ONLY on the inner content wrapper: + the outer flex container is invisible, so animating its frame + adds nothing visually — but on Fabric a layout-animated height + goes through shadow-tree updates and can perturb the SIBLING + block below mid-transition (the bottom button jolted). The + inner wrapper alone is what makes the centered amount glide. */} + + + + + + {lastHeroErrorRef.current ?? ''} + + + + } + width="100%" + /> + + + {mode === 'review' ? ( + + + + + + + ) : ( + + + {/* One slot, two states: presets while the amount is empty + (tapping one jumps straight to review); a typed amount + morphs the slot into the preview CTA. Clearing the amount + brings the presets back. Both render at the large-button + height, so the swap never reflows the keypad below. */} + {amount > 0 ? ( + + ) : ( + + )} + + + + )} + + + + ); +} + +export default HeadlessBuyPage; diff --git a/packages/kit/src/views/FiatCrypto/router/index.tsx b/packages/kit/src/views/FiatCrypto/router/index.tsx index c5315a99adb2..f0c422a01594 100644 --- a/packages/kit/src/views/FiatCrypto/router/index.tsx +++ b/packages/kit/src/views/FiatCrypto/router/index.tsx @@ -12,6 +12,19 @@ const DeriveTypesAddress = LazyLoadPage( import('@onekeyhq/kit/src/views/WalletAddress/pages/DeriveTypesAddress'), ); +const FiatCryptoHeadlessBuyModal = LazyLoadPage( + () => import('@onekeyhq/kit/src/views/FiatCrypto/pages/HeadlessBuy'), +); + +const FiatCryptoHeadlessBuyTokenSelectorModal = LazyLoadPage( + () => + import('@onekeyhq/kit/src/views/FiatCrypto/pages/HeadlessBuy/TokenSelector'), +); + +const FiatCryptoHeadlessBuySuccessModal = LazyLoadPage( + () => import('@onekeyhq/kit/src/views/FiatCrypto/pages/HeadlessBuy/Success'), +); + export const ModalFiatCryptoRouter: IModalFlowNavigatorConfig< EModalFiatCryptoRoutes, IModalFiatCryptoParamList @@ -24,4 +37,16 @@ export const ModalFiatCryptoRouter: IModalFlowNavigatorConfig< name: EModalFiatCryptoRoutes.DeriveTypesAddress, component: DeriveTypesAddress, }, + { + name: EModalFiatCryptoRoutes.HeadlessBuy, + component: FiatCryptoHeadlessBuyModal, + }, + { + name: EModalFiatCryptoRoutes.HeadlessBuyTokenSelector, + component: FiatCryptoHeadlessBuyTokenSelectorModal, + }, + { + name: EModalFiatCryptoRoutes.HeadlessBuySuccess, + component: FiatCryptoHeadlessBuySuccessModal, + }, ]; diff --git a/packages/kit/src/views/FiatCrypto/utils/openFiatCryptoOrHeadless.ts b/packages/kit/src/views/FiatCrypto/utils/openFiatCryptoOrHeadless.ts new file mode 100644 index 000000000000..d26690a6d5d1 --- /dev/null +++ b/packages/kit/src/views/FiatCrypto/utils/openFiatCryptoOrHeadless.ts @@ -0,0 +1,72 @@ +import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy'; +import appGlobals from '@onekeyhq/shared/src/appGlobals'; +import { canUseHeadless } from '@onekeyhq/shared/src/modules3rdParty/onramper'; +import { EModalRoutes, ERootRoutes } from '@onekeyhq/shared/src/routes'; +import { EModalFiatCryptoRoutes } from '@onekeyhq/shared/src/routes/fiatCrypto'; +import type { IFiatCryptoToken } from '@onekeyhq/shared/types/fiatCrypto'; + +export type ITryOpenHeadlessBuyParams = { + networkId: string; + tokenAddress: string; + accountId?: string; + // Provided when navigating from the token list; direct-buy entries omit it and + // the token is resolved from the cached list. + token?: IFiatCryptoToken; +}; + +// The single choke point for every buy entry. Returns true if it navigated to +// the native Headless buy page (the caller must stop); returns false when the +// native path isn't available, so the caller runs its existing web-widget flow +// unchanged. Buy-only — sell always stays on the web widget. +// +// Spliced in ahead of `generateWidgetUrl` at every buy entry — keep this list +// in sync when adding an 8th: ActionBuy's openFiatCryptoWidget (also serves +// the aggregate Overview tab), SellOrBuy list, WalletActionBuy, +// WalletActions/index, Market tradeHook, Send SendAmountInputContainer, +// ReceiveSelector. +export async function tryOpenHeadlessBuy({ + networkId, + tokenAddress, + accountId, + token, +}: ITryOpenHeadlessBuyParams): Promise { + if (!canUseHeadless()) { + return false; + } + + // The native flow pays out to the account's own address — without an + // account there is no destination to quote against (several entries pass + // '' when none is active). The web widget collects an address itself, so + // it keeps handling that case. + if (!accountId) { + return false; + } + + // The server (OK-58060) is the single source of truth: `headlessSupported` + // is region-trimmed by request IP, and `onramperNetworkCode` is the network + // slug the checkout request needs — a token missing either can never quote, + // so the caller's web-widget flow runs unchanged for it. A caller-provided + // token already carries the server fields (same list endpoint), so only + // token-less direct-buy entries pay the list lookup. + let resolvedToken = token; + if (resolvedToken === undefined) { + resolvedToken = + await backgroundApiProxy.serviceFiatCrypto.getHeadlessBuyToken({ + networkId, + tokenAddress, + accountId, + }); + } + if (!resolvedToken?.headlessSupported || !resolvedToken.onramperNetworkCode) { + return false; + } + + appGlobals.$navigationRef.current?.navigate(ERootRoutes.Modal, { + screen: EModalRoutes.FiatCryptoModal, + params: { + screen: EModalFiatCryptoRoutes.HeadlessBuy, + params: { networkId, accountId, tokenAddress, token: resolvedToken }, + }, + }); + return true; +} diff --git a/packages/kit/src/views/Home/components/WalletActions/WalletActionBuy.tsx b/packages/kit/src/views/Home/components/WalletActions/WalletActionBuy.tsx index acd34f6ec350..bc23db5c9f8c 100644 --- a/packages/kit/src/views/Home/components/WalletActions/WalletActionBuy.tsx +++ b/packages/kit/src/views/Home/components/WalletActions/WalletActionBuy.tsx @@ -13,6 +13,7 @@ import { useActiveAccount } from '@onekeyhq/kit/src/states/jotai/contexts/accoun import { useHomeTokenListSnapshot } from '@onekeyhq/kit/src/states/jotai/contexts/tokenList/cells'; import { showBotWalletDisabledToast } from '@onekeyhq/kit/src/utils/botWalletDisabledToast'; import { useFiatCrypto } from '@onekeyhq/kit/src/views/FiatCrypto/hooks'; +import { tryOpenHeadlessBuy } from '@onekeyhq/kit/src/views/FiatCrypto/utils/openFiatCryptoOrHeadless'; import { WALLET_TYPE_WATCHING } from '@onekeyhq/shared/src/consts/dbConsts'; import { ETranslations } from '@onekeyhq/shared/src/locale'; import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; @@ -190,6 +191,15 @@ export function WalletActionBuy({ tokenSymbol: nativeToken.symbol, networkID: network?.id ?? '', }); + if ( + await tryOpenHeadlessBuy({ + networkId: network?.id ?? '', + tokenAddress: nativeToken.address, + accountId: a?.id ?? '', + }) + ) { + return; + } const { url } = await backgroundApiProxy.serviceFiatCrypto.generateWidgetUrl({ networkId: network?.id ?? '', diff --git a/packages/kit/src/views/Home/components/WalletActions/index.tsx b/packages/kit/src/views/Home/components/WalletActions/index.tsx index 014898e99c2b..6d558f148da9 100644 --- a/packages/kit/src/views/Home/components/WalletActions/index.tsx +++ b/packages/kit/src/views/Home/components/WalletActions/index.tsx @@ -20,6 +20,7 @@ import { useTokenListStateAtom } from '@onekeyhq/kit/src/states/jotai/contexts/t import { useHomeTokenListSnapshot } from '@onekeyhq/kit/src/states/jotai/contexts/tokenList/cells'; import { showBotWalletDisabledToast } from '@onekeyhq/kit/src/utils/botWalletDisabledToast'; import { shouldBlockBotWalletReceive } from '@onekeyhq/kit/src/utils/botWalletStatusUtils'; +import { tryOpenHeadlessBuy } from '@onekeyhq/kit/src/views/FiatCrypto/utils/openFiatCryptoOrHeadless'; import { ETranslations } from '@onekeyhq/shared/src/locale'; import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; import type { IModalSendParamList } from '@onekeyhq/shared/src/routes'; @@ -195,6 +196,15 @@ function WalletActionSend({ safeResolve(false); void dialogRef.close(); try { + if ( + await tryOpenHeadlessBuy({ + networkId: network.id, + tokenAddress: '', + accountId: account?.id ?? '', + }) + ) { + return; + } const { url } = await backgroundApiProxy.serviceFiatCrypto.generateWidgetUrl( { diff --git a/packages/kit/src/views/Market/components/tradeHook.tsx b/packages/kit/src/views/Market/components/tradeHook.tsx index 67ba310068fa..edb32cbcff09 100644 --- a/packages/kit/src/views/Market/components/tradeHook.tsx +++ b/packages/kit/src/views/Market/components/tradeHook.tsx @@ -36,6 +36,7 @@ import backgroundApiProxy from '../../../background/instance/backgroundApiProxy' import useAppNavigation from '../../../hooks/useAppNavigation'; import { useActiveAccount } from '../../../states/jotai/contexts/accountSelector'; import { EarnNavigation } from '../../Earn/earnUtils'; +import { tryOpenHeadlessBuy } from '../../FiatCrypto/utils/openFiatCryptoOrHeadless'; export const useMarketTradeNetwork = (token: IMarketTokenDetail | null) => { const { detailPlatforms, platforms = {} } = token || {}; @@ -151,6 +152,17 @@ export const useMarketTradeActions = (token: IMarketTokenDetail | null) => { return; } + if ( + type === 'buy' && + (await tryOpenHeadlessBuy({ + networkId, + tokenAddress: realContractAddress, + accountId: networkAccount?.id, + })) + ) { + return; + } + const { url, build } = await backgroundApiProxy.serviceFiatCrypto.generateWidgetUrl({ networkId, diff --git a/packages/kit/src/views/Receive/pages/ReceiveSelector.tsx b/packages/kit/src/views/Receive/pages/ReceiveSelector.tsx index a58f93b9cf3c..c6c3bd55978c 100644 --- a/packages/kit/src/views/Receive/pages/ReceiveSelector.tsx +++ b/packages/kit/src/views/Receive/pages/ReceiveSelector.tsx @@ -47,6 +47,7 @@ import useAppNavigation from '../../../hooks/useAppNavigation'; import { useExchangeAppDetection } from '../../../hooks/useExchangeAppDetection'; import { usePromiseResult } from '../../../hooks/usePromiseResult'; import { useActiveAccount } from '../../../states/jotai/contexts/accountSelector'; +import { tryOpenHeadlessBuy } from '../../FiatCrypto/utils/openFiatCryptoOrHeadless'; import { HomeTokenListProviderMirror } from '../../Home/components/HomeTokenListProvider/HomeTokenListProviderMirror'; import { WalletActionBuy } from '../../Home/components/WalletActions/WalletActionBuy'; import { WalletActionReceive } from '../../Home/components/WalletActions/WalletActionReceive'; @@ -185,8 +186,17 @@ function ReceiveSelectorContent() { ); const handleBuyOnPress = useCallback( - ({ onPress }: { onPress: () => void }) => { + async ({ onPress }: { onPress: () => void }) => { if (token && isSupported && url) { + if ( + await tryOpenHeadlessBuy({ + networkId: networkId ?? '', + tokenAddress: token.address, + accountId, + }) + ) { + return; + } if (platformEnv.isDesktop || platformEnv.isNative) { openFiatCryptoUrl(url); } else { @@ -196,7 +206,7 @@ function ReceiveSelectorContent() { onPress(); } }, - [token, isSupported, url], + [token, isSupported, url, networkId, accountId], ); const handleSwapOnPress = useCallback(() => { diff --git a/packages/kit/src/views/Send/components/SendAutoSizeAmountInput/index.tsx b/packages/kit/src/views/Send/components/SendAutoSizeAmountInput/index.tsx index 88909b2f237c..803ca920f87d 100644 --- a/packages/kit/src/views/Send/components/SendAutoSizeAmountInput/index.tsx +++ b/packages/kit/src/views/Send/components/SendAutoSizeAmountInput/index.tsx @@ -20,7 +20,11 @@ import { useMedia, useTheme, } from '@onekeyhq/components'; -import type { IInputProps, IStackProps } from '@onekeyhq/components'; +import type { + IInputProps, + ISizableTextProps, + IStackProps, +} from '@onekeyhq/components'; import { webFontFamily } from '@onekeyhq/components/src/utils/webFontFamily'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; import type { NUMBER_FORMATTER } from '@onekeyhq/shared/src/utils/numberUtils'; @@ -42,6 +46,24 @@ const AMOUNT_FONT_FAMILY = platformEnv.isNative // iOS-only hidden marker used to force a native prop delta without visible UI change. const IOS_FORCE_WRITE_BACK_MARKER = '\u200B'; +// Size token \u2192 the Skeleton variant whose total height matches that token's +// lineHeight (the Skeleton statics are built on that contract). +const VALUE_SIZE_SKELETONS: Record< + string, + (props: { children?: React.ReactNode }) => React.ReactNode +> = { + '$bodySm': Skeleton.BodySm, + '$bodyMd': Skeleton.BodyMd, + '$bodyMdMedium': Skeleton.BodyMd, + '$bodyLg': Skeleton.BodyLg, + '$bodyLgMedium': Skeleton.BodyLg, + '$headingSm': Skeleton.HeadingSm, + '$headingMd': Skeleton.HeadingMd, + '$headingLg': Skeleton.HeadingLg, + '$headingXl': Skeleton.HeadingXl, + '$heading2xl': Skeleton.Heading2Xl, +}; + const stripIOSForceWriteBackMarker = (text: string) => text.replace(/\u200B/g, ''); @@ -183,6 +205,8 @@ type ISendAmountAutoSizeInputProps = { valueProps?: { value?: string; color?: string; + // Typography of the converted-value line. Defaults to '$headingLg'. + size?: ISizableTextProps['size']; onPress?: () => void; loading?: boolean; currency?: string; @@ -400,6 +424,13 @@ function SendAutoSizeAmountInputComponent( autoSizeValue = platformEnv.isNativeIOS ? '0' : ''; } + const valueSize = valueProps?.size ?? '$headingLg'; + // Typography-matched: each Skeleton variant's total height equals the same + // token's lineHeight, so the loading toggle never shifts layout. Keep this + // map covering every size the prop accepts in practice. + const ValueSkeleton = + VALUE_SIZE_SKELETONS[valueSize as string] ?? Skeleton.HeadingLg; + const amountInputNode = isLoading ? ( @@ -481,7 +512,7 @@ function SendAutoSizeAmountInputComponent( })} > {valueProps?.loading ? ( - + ) : ( <> { setIsBuyLoading(true); try { + if ( + await tryOpenHeadlessBuy({ + networkId, + tokenAddress: tokenInfo?.address ?? '', + accountId: currentAccountId, + }) + ) { + return; + } const { url } = await backgroundApiProxy.serviceFiatCrypto.generateWidgetUrl({ networkId, diff --git a/packages/shared/src/logger/scopes/fiatCrypto/index.ts b/packages/shared/src/logger/scopes/fiatCrypto/index.ts index 65e2ad0c9f03..baf6d24c43a1 100644 --- a/packages/shared/src/logger/scopes/fiatCrypto/index.ts +++ b/packages/shared/src/logger/scopes/fiatCrypto/index.ts @@ -1,10 +1,13 @@ import { BaseScope } from '../../base/baseScope'; import { EScopeName } from '../../types'; +import { OnramperScene } from './scenes/onramper'; import { RequestScene } from './scenes/request'; export class FiatCryptoScope extends BaseScope { protected override scopeName = EScopeName.fiatCrypto; request = this.createScene('request', RequestScene); + + onramper = this.createScene('onramper', OnramperScene); } diff --git a/packages/shared/src/logger/scopes/fiatCrypto/scenes/onramper.ts b/packages/shared/src/logger/scopes/fiatCrypto/scenes/onramper.ts new file mode 100644 index 000000000000..208995c92e8c --- /dev/null +++ b/packages/shared/src/logger/scopes/fiatCrypto/scenes/onramper.ts @@ -0,0 +1,53 @@ +import { BaseScene } from '../../../base/baseScene'; +import { LogToServer } from '../../../base/decorators'; + +// Funnel events for the native Onramper Headless buy flow. +export class OnramperScene extends BaseScene { + @LogToServer() + public enterAmountPage(params: { networkId: string; tokenSymbol: string }) { + return params; + } + + @LogToServer() + public quoteReceived(params: { + networkId: string; + tokenSymbol: string; + amount: number; + // Onramper quote id — pairs with checkoutId for server-side order tracing. + quoteId?: string; + }) { + return params; + } + + @LogToServer() + public checkoutCompleted(params: { + networkId: string; + tokenSymbol: string; + // Onramper checkout id — the server-side handle for locating this order + // with Onramper support (device logs don't persist). + checkoutId?: string; + }) { + return params; + } + + @LogToServer() + public checkoutFailed(params: { + networkId: string; + tokenSymbol: string; + errorCode?: string; + // Present only when the SDK attaches it (post-checkout failures); + // quote-stage failures have no checkout yet. + checkoutId?: string; + }) { + return params; + } + + @LogToServer() + public webFallbackShown(params: { + networkId: string; + tokenSymbol: string; + errorCode?: string; + }) { + return params; + } +} diff --git a/packages/shared/src/modules3rdParty/onramper/NATIVE_SETUP.md b/packages/shared/src/modules3rdParty/onramper/NATIVE_SETUP.md new file mode 100644 index 000000000000..8c9311ab5a50 --- /dev/null +++ b/packages/shared/src/modules3rdParty/onramper/NATIVE_SETUP.md @@ -0,0 +1,78 @@ +# Onramper Headless SDK — native activation + +The **JS side is already wired**: the dep is declared, `realClient.native.ts` +adapts the real SDK, and `createOnramperClient()` returns the real client on a +real device / the mock on the Simulator (no App Attest there). What's left is the +native install + a couple of values, all below. + +Package: **`@onramper/onramper-react-native@1.1.1`** (already in +`apps/mobile/package.json`). + +## ⚠️ nitro bump — verify the OneKey Nitro modules + +`@onramper/onramper-react-native@1.1.1` peers: react ≥19 ✅, react-native ≥0.79 ✅, +react-native-nitro-modules ≥0.35.0. Already bumped **0.33.2 → 0.35.2** (see +`patches/react-native-nitro-modules+0.35.2.patch`) in `apps/mobile/package.json`. + +That nitro version is shared by the OneKey Nitro modules +(`@onekeyfe/react-native-{keychain-module,cloud-kit-module,device-utils,perp-depth-bar}`, +pinned at 3.0.78, generated against nitro 0.33.2). **The native team must rebuild / +re-verify those after `pod install`** — a nitro minor bump can change the +nitrogen-generated native interface, so those modules may need versions built +against 0.35.x. If the iOS native build fails on one of them, this is why. + +## Steps + +1. **Install JS deps** (dep already declared): + ```bash + yarn + ``` +2. **nitro** is already bumped to `0.35.2` (only in `apps/mobile/package.json`; + no `resolutions`). After `pod install`, confirm the 4 `@onekeyfe` Nitro modules + still build (see the nitro note above) — rebuild them against 0.35.x if not. +3. **Delete the type shim** — `packages/shared/src/modules3rdParty/onramper/onramper-sdk.d.ts`. + It only exists so the repo type-checks before install; the real package ships + its own types and will conflict with it. +4. **Pod install**: + ```bash + cd apps/mobile/ios && pod install + ``` + Commit the `Podfile.lock` diff (an autolinked `OnramperReactNative` pod + + `NitroModules` dependency). +5. **App Attest entitlement** — already wired per build configuration: the + entitlements file carries `$(ONEKEY_APPATTEST_ENVIRONMENT)` and the app + target defines it as `development` (Debug) / `production` (Release, which + every EAS profile uses). Ad hoc QA builds are NOT on Apple's list of + always-production distributions, so the explicit Release value matters. + Enforced at build time: the "[OneKey] Assert App Attest environment" build + phase fails any Release build where the value is not `production`. +6. **Production credentials** are injected at bundle time, never committed + (OK-59538): `realClient.native.ts` reads `process.env.ONRAMPER_CLIENT_ID` + / `ONRAMPER_API_KEY` (allowlisted in `development/envExposedToClient.js`). + CI: GitHub secrets appended to `.env.expo` by `release-ios.yml` before the + EAS upload. Local production-profile build: put both in the git-ignored + root `.env` (never `.env.expo` / `.env.version` — those are committed). + Verified against SDK 1.1.1: `configure()` requires BOTH — `apiKey` is the + *publishable* `pk_...` key (the hosted widget embeds the same key in its + URL), not the backend partner secret. `environment` only accepts + `'development' | 'production'` (anything else is coerced to production on + the Swift side). Unset values keep production builds on the web widget + (`hasOnramperCredentials()` fail-closed). +7. **Verify the SDK surface** against the installed types + (`node_modules/@onramper/onramper-react-native/lib/typescript/...`): confirm + the `OnramperClient` export name + method signatures in `realClient.native.ts`, + and adjust `type.ts`'s `IOnramperQuote` / `IOnramperCheckoutRequest` to the + real shapes (plan open question #3). Set `ONRAMPER_MIN_IOS` in `index.native.ts` + to the SDK's real minimum. + +## Verify (real device) + +- A token with `headlessSupported: true` (backend flag) routes to the native page; + the **real Apple Pay button** renders; the real quote/ETA fill in. +- Tap → ToS → (login) → Apple Pay sheet → `completed` closes the modal + toast. +- Non-USD / unsupported / structural failure → web-widget fallback. +- To reach the page before the backend flag is live, use the Developer Gallery + "Headless Buy (mock)" entry (it bypasses the flag; on device it uses the real + client since `ExpoDevice.isDevice` is true). + +CI / e2e (no device) keep running the mock — real-SDK e2e must be a device-only job. diff --git a/packages/shared/src/modules3rdParty/onramper/index.native.ts b/packages/shared/src/modules3rdParty/onramper/index.native.ts new file mode 100644 index 000000000000..d684b5dedce5 --- /dev/null +++ b/packages/shared/src/modules3rdParty/onramper/index.native.ts @@ -0,0 +1,53 @@ +import * as ExpoDevice from 'expo-device'; +import { Platform } from 'react-native'; + +import platformEnv from '../../platformEnv'; + +import { createMockOnramperClient } from './mock'; +import { + createRealOnramperClient, + hasOnramperCredentials, +} from './realClient.native'; + +import type { ICreateOnramperClientParams, IOnramperClient } from './type'; + +export * from './type'; +export * from './utils'; +export { getOnramperConfig } from './realClient.native'; + +// Minimum iOS version the Onramper Headless SDK supports — its pod spec pins +// `:ios => '16.0'`. The app deployment target matches (16.0), so this guard +// only matters if that floor ever diverges again. +const ONRAMPER_MIN_IOS = 16; + +export function canUseHeadless(): boolean { + if (!platformEnv.isNativeIOS) { + return false; + } + // A build flavor without its credentials pair (production until the + // post-KYB key lands) must stay on the web widget — the real client throws + // at creation when credentials are missing. + if (!hasOnramperCredentials()) { + return false; + } + const iosVersion = parseInt(String(Platform.Version), 10); + if (Number.isNaN(iosVersion) || iosVersion < ONRAMPER_MIN_IOS) { + return false; + } + // App Attest requires a real device; allow the Simulator only in dev builds so + // the mock-backed UI stays reviewable without a device. + return ExpoDevice.isDevice || Boolean(platformEnv.isDev); +} + +export function createOnramperClient( + params: ICreateOnramperClientParams, +): IOnramperClient { + // Real SDK only on real iOS devices. Android has no OnramperReactNative + // native module, so creating the real client there throws synchronously + // during render (reachable via the dev Gallery, which bypasses + // canUseHeadless); the Simulator has no App Attest. Both use the mock. + // See ./NATIVE_SETUP.md. + return platformEnv.isNativeIOS && ExpoDevice.isDevice + ? createRealOnramperClient(params) + : createMockOnramperClient(params); +} diff --git a/packages/shared/src/modules3rdParty/onramper/index.ts b/packages/shared/src/modules3rdParty/onramper/index.ts new file mode 100644 index 000000000000..8717ed577e00 --- /dev/null +++ b/packages/shared/src/modules3rdParty/onramper/index.ts @@ -0,0 +1,30 @@ +import { OneKeyLocalError } from '../../errors'; + +import type { + ICreateOnramperClientParams, + IOnramperClient, + IOnramperConfig, +} from './type'; + +export * from './type'; +export * from './utils'; + +// The Headless SDK is native-iOS-only. Web/desktop always report unsupported so +// the branch helper routes to the existing web widget. +export function canUseHeadless(): boolean { + return false; +} + +// Never called on web (the page is native-only); present so the shared import +// resolves on every platform. +export function getOnramperConfig(): IOnramperConfig { + return { apiKey: '', clientId: '', environment: 'development' }; +} + +export function createOnramperClient( + _params: ICreateOnramperClientParams, +): IOnramperClient { + throw new OneKeyLocalError( + 'Onramper Headless SDK is only available on native iOS devices', + ); +} diff --git a/packages/shared/src/modules3rdParty/onramper/mock.ts b/packages/shared/src/modules3rdParty/onramper/mock.ts new file mode 100644 index 000000000000..be45069b7265 --- /dev/null +++ b/packages/shared/src/modules3rdParty/onramper/mock.ts @@ -0,0 +1,84 @@ +import timerUtils from '../../utils/timerUtils'; + +import type { + ICreateOnramperClientParams, + IOnramperCheckoutRequest, + IOnramperClient, + IOnramperEventListener, + IOnramperEventName, +} from './type'; + +const delay = (ms: number) => timerUtils.wait(ms); + +function mockError(code: string, message: string) { + const err = new Error(message) as Error & { code: string }; + err.code = code; + return err; +} + +// Mock client so the whole buy UI can be built and reviewed on Simulator with no +// native package. Returns `button: null` (the action zone renders our own OneKey +// button in mock mode) and a fake quote derived from the amount. Dev-only +// forced-error hooks for reviewing S4/S5 use deliberately implausible amounts +// so they can't shadow genuine input ($1 used to force S5, which collided with +// real below-minimum tests): 111111 → structural, 222222 → retryable. +export function createMockOnramperClient( + _params: ICreateOnramperClientParams, +): IOnramperClient { + const listeners: Partial< + Record> + > = {}; + + return { + isMock: true, + async initialize() { + await delay(300); + }, + async getCheckoutRequirements(request: IOnramperCheckoutRequest) { + await delay(500); + if (request.amount === 111_111) { + throw mockError('checkoutForbidden', 'Region blocked (mock)'); + } + if (request.amount === 222_222) { + throw mockError('temporaryFailure', 'Please try again (mock)'); + } + const rate = 192.94; + const networkFee = 0.42; + const transactionFee = 3.99; + return { + button: null, + quote: { + quoteId: 'mock-quote', + ramp: 'coinbase', + rate, + payout: + Math.max(request.amount - networkFee - transactionFee, 0) / rate, + paymentMethod: 'applepay', + networkFee, + transactionFee, + }, + }; + }, + addEventListener( + name: IOnramperEventName, + listener: IOnramperEventListener, + ) { + const set = (listeners[name] ??= new Set()); + set.add(listener); + return () => { + set.delete(listener); + }; + }, + async reset() { + // no-op for the mock + }, + async signOut() { + // no-op for the mock + }, + destroy() { + (Object.keys(listeners) as IOnramperEventName[]).forEach((name) => { + listeners[name]?.clear(); + }); + }, + }; +} diff --git a/packages/shared/src/modules3rdParty/onramper/realClient.native.ts b/packages/shared/src/modules3rdParty/onramper/realClient.native.ts new file mode 100644 index 000000000000..59eb97531e6b --- /dev/null +++ b/packages/shared/src/modules3rdParty/onramper/realClient.native.ts @@ -0,0 +1,169 @@ +import type { ReactNode } from 'react'; + +import { OnramperClient } from '@onramper/onramper-react-native'; + +import { OneKeyLocalError } from '../../errors'; +import platformEnv from '../../platformEnv'; + +import type { + ICreateOnramperClientParams, + IOnramperClient, + IOnramperConfig, + IOnramperEventListener, + IOnramperEventName, +} from './type'; + +// Onramper credentials. BOTH are required by the SDK's configure() — apiKey is +// the publishable key (the hosted widget embeds the same key in its URL, it is +// not the backend partner secret), clientId identifies the partner app. +// Staging and production use DIFFERENT pairs, so the pair is selected with the +// same isDev switch as `environment` — a production build can never ship the +// staging key by accident. +const STAGING_CREDENTIALS = { + clientId: '01KJD2DBBGF9Q8G133QK2A3DC1', + apiKey: 'pk_test_01KWHMBRP4ABPB5DE4EB0CHNC6', +}; +// Production pair is injected at bundle time (OK-59538 forbids committing it +// in plaintext): CI appends ONRAMPER_CLIENT_ID / ONRAMPER_API_KEY from GitHub +// secrets to .env.expo (release-ios.yml → shared-env action) before the EAS +// upload; local production-profile builds read them from the git-ignored root +// .env. Both names must stay in envExposedToClient.js for the babel inline to +// happen. When unset, hasOnramperCredentials() keeps production builds on the +// web widget instead of crashing at client creation. +const PRODUCTION_CREDENTIALS = { + clientId: process.env.ONRAMPER_CLIENT_ID ?? '', + apiKey: process.env.ONRAMPER_API_KEY ?? '', +}; + +function getCredentials() { + return platformEnv.isDev ? STAGING_CREDENTIALS : PRODUCTION_CREDENTIALS; +} + +export function hasOnramperCredentials(): boolean { + const credentials = getCredentials(); + return Boolean(credentials.apiKey && credentials.clientId); +} + +export function getOnramperConfig(): IOnramperConfig { + return { + ...getCredentials(), + // The SDK only knows 'development' | 'production'; any other string is + // silently coerced to production on the Swift side, so map dev builds to + // 'development' explicitly (our staging session tokens only work there). + environment: platformEnv.isDev ? 'development' : 'production', + theme: 'system', + }; +} + +// SDK checkout events nest the failure under `error` ({ error: { code, message, +// info } }); flatten to the IOnramperEvent shape the kit layer consumes. +type ISdkEventPayload = { + checkoutId?: string; + error?: { code?: string; message?: string; info?: Record }; +}; + +// Mirrors the SDK's `OnramperErrorCode` union (1.1.1) — used to validate codes +// recovered from degraded bridge errors below. +const KNOWN_ERROR_CODES = new Set([ + 'notInitialized', + 'initializationFailed', + 'attestationFailed', + 'invalidStateTransition', + 'invalidState', + 'networkError', + 'decodingError', + 'timeout', + 'requirementNotSatisfied', + 'amountOutOfRange', + 'oidcFlowCancelled', + 'oidcTokenExchangeFailed', + 'oidcFlowFailed', + 'userTokenInvalid', + 'userTokenRefreshFailed', + 'webviewLoadFailed', + 'deepLinkFailed', + 'invalidRequest', + 'quoteUnavailable', + 'checkoutForbidden', + 'temporaryFailure', + 'unrecoverable', + 'configurationError', + 'deviceBlocked', + 'securityStorageFailed', + 'securityTrustFailed', + 'platformUnsupported', + 'intentInvalidated', + 'intentAlreadyConsumed', + 'clientAlreadyConfigured', + 'sessionExpirationHandlerFailed', +]); + +// The Nitro bridge drops the structured payload on thrown rejections: JS +// receives a plain Error whose message is the Swift enum description, e.g. +// `quoteUnavailable(debugInfo: Optional("OnramperBackend-40003: …"))`, and the +// JS wrapper stamps it code 'unrecoverable' (verified on device 2026-07-15). +// Recover the real case name from the message prefix so error classification +// and user-facing copy stay correct on the thrown path (the parallel `failed` +// event keeps the structured code, but the quote loop consumes the throw). +function withRecoveredErrorCode(error: unknown): unknown { + const e = error as { code?: string; message?: string }; + if (!e || typeof e !== 'object') { + return error; + } + if (e.code && e.code !== 'unrecoverable') { + return error; + } + const match = /^([a-z][a-zA-Z0-9]*)\s*(?:\(|$)/.exec(e.message ?? ''); + const recovered = match?.[1]; + if (recovered && KNOWN_ERROR_CODES.has(recovered)) { + e.code = recovered; + } + return error; +} + +// Adapts the real Onramper SDK client to IOnramperClient. +export function createRealOnramperClient( + params: ICreateOnramperClientParams, +): IOnramperClient { + if (!params.apiKey || !params.clientId) { + throw new OneKeyLocalError( + 'Onramper credentials missing: fill ONRAMPER_API_KEY / ONRAMPER_CLIENT_ID in realClient.native.ts', + ); + } + const client = new OnramperClient({ + apiKey: params.apiKey, + clientId: params.clientId, + environment: params.environment, + theme: params.theme ?? 'system', + logLevel: platformEnv.isDev ? 'debug' : 'off', + onSessionExpired: params.onSessionExpired, + }); + return { + isMock: false, + initialize: (session) => client.initialize(session), + getCheckoutRequirements: async (request, buttonStyle) => { + try { + const res = await client.getCheckoutRequirements(request, buttonStyle); + return { button: res.button as ReactNode, quote: res.quote }; + } catch (error) { + throw withRecoveredErrorCode(error); + } + }, + addEventListener: ( + name: IOnramperEventName, + listener: IOnramperEventListener, + ) => + client.addEventListener(name, (event: unknown) => { + const e = event as ISdkEventPayload; + listener({ + checkoutId: e.checkoutId, + errorCode: e.error?.code, + message: e.error?.message, + info: e.error?.info, + }); + }), + reset: () => client.reset(), + signOut: () => client.signOut(), + destroy: () => client.destroy(), + }; +} diff --git a/packages/shared/src/modules3rdParty/onramper/type.ts b/packages/shared/src/modules3rdParty/onramper/type.ts new file mode 100644 index 000000000000..0b68040d2abf --- /dev/null +++ b/packages/shared/src/modules3rdParty/onramper/type.ts @@ -0,0 +1,120 @@ +import type { ReactNode } from 'react'; + +// The SDK only accepts these two values; anything else is silently coerced to +// production on the Swift side (HybridOnramperNitro.swift), so never widen this. +export type IOnramperEnvironment = 'production' | 'development'; + +export type IOnramperSession = { + sessionId: string; + sessionToken: string; + // Extra fields the backend returns; carried through to the SDK. Optional so the + // mock session ({ sessionId, sessionToken }) still satisfies the type. + tokenFamilyId?: string; + expiresAt?: string; +}; + +export type IOnramperConfig = { + // Publishable key (pk_...), NOT the backend partner secret. Required: the + // SDK's configure() takes `apiKey: string` and passes it straight to the + // native OnramperConfiguration — undefined fails at the Nitro boundary. + apiKey: string; + clientId: string; + environment: IOnramperEnvironment; + theme?: 'system' | 'light' | 'dark'; +}; + +export type IOnramperButtonStyle = { + backgroundColor: string; + foregroundColor: string; + borderRadius: number; +}; + +export type IOnramperCheckoutRequest = { + source: string; // fiat currency code, e.g. 'usd' + destination: string; // crypto asset code, e.g. 'sol' + amount: number; // denominated in the source fiat + type: 'buy'; + paymentMethod: 'applepay'; + // ISO country/subdivision codes (lowercase). Omit to let Onramper geo-detect. + country?: string; + subdivision?: string; + // Restrict routing to these provider slugs (SDK `onlyOnramps`). Omit to let + // Onramper pick the best provider. + onlyOnramps?: string[]; + wallet: { network: string; address: string }; +}; + +// Mirrors the SDK's `QuoteResponse` (pinned against 1.1.1 source): the backend +// only returns a *successful* quote — a request that can't be priced throws +// (e.g. `quoteUnavailable`) instead of returning a partial quote, so pricing +// fields are always present. There is no ETA field. Fees are denominated in the +// source fiat. `rate` is crypto-per-fiat and equals payout/amount exactly +// (device-verified 2026-07-17, coinbasepay: payout 0.05116585 / $100 → +// rate 0.0005116585) — it carries no information beyond the quote itself, so +// never use it to price the payout in fiat; use OneKey's own market feed. +export type IOnramperQuote = { + quoteId: string; + ramp: string; // provider slug, e.g. 'coinbase' + rate: number; + payout: number; + paymentMethod: string; + networkFee: number; + transactionFee: number; + recommendations?: string[]; +}; + +export type IOnramperCheckoutRequirements = { + button: ReactNode; + quote: IOnramperQuote; +}; + +export type IOnramperEventName = + | 'completed' + | 'failed' + | 'paymentAuthorized' + | 'paymentProcessing' + | 'loginRequired'; + +export type IOnramperEvent = { + checkoutId?: string; + errorCode?: string; + message?: string; + info?: Record; +}; + +export type IOnramperError = { + code?: string; + message?: string; + info?: Record; +}; + +export type IOnramperEventListener = (event: IOnramperEvent) => void; + +export type IOnramperClient = { + // True for the mock client used on Simulator / non-device dev builds. + isMock?: boolean; + initialize: (session: IOnramperSession) => Promise; + getCheckoutRequirements: ( + request: IOnramperCheckoutRequest, + buttonStyle?: IOnramperButtonStyle, + ) => Promise; + addEventListener: ( + name: IOnramperEventName, + listener: IOnramperEventListener, + ) => () => void; + reset: () => Promise; + signOut: () => Promise; + destroy: () => void; +}; + +export type ICreateOnramperClientParams = IOnramperConfig & { + // Invoked by the SDK when the partner session is about to expire; must return + // a freshly minted pair (from the OneKey backend) without user interaction. + onSessionExpired: () => Promise; +}; + +export type ICanUseHeadless = () => boolean; + +export type ICreateOnramperClient = ( + params: ICreateOnramperClientParams, +) => IOnramperClient; diff --git a/packages/shared/src/modules3rdParty/onramper/utils.ts b/packages/shared/src/modules3rdParty/onramper/utils.ts new file mode 100644 index 000000000000..df98174991c9 --- /dev/null +++ b/packages/shared/src/modules3rdParty/onramper/utils.ts @@ -0,0 +1,23 @@ +import type { IOnramperError } from './type'; + +// Structural / non-retryable failures → fall back to the web widget (S5). +// Everything else is treated as retryable (inline retry, S4). +// NOTE: 'unrecoverable' is deliberately NOT in this set. The RN wrapper stamps +// it as the DEFAULT code on any rejection whose structured code was lost at the +// native bridge (OnramperError.from), so user-fixable request errors (e.g. a +// below-minimum amount) can reach us as 'unrecoverable'. Unknown must stay +// retryable; only codes the SDK explicitly names as terminal go to the widget. +const STRUCTURAL_ERROR_CODES = new Set([ + 'checkoutForbidden', + 'deviceBlocked', + 'configurationError', + // App Attest failed on this device — retrying cannot fix it. + 'attestationFailed', + 'platformUnsupported', +]); + +export function isStructuralOnramperError( + error: IOnramperError | undefined, +): boolean { + return Boolean(error?.code && STRUCTURAL_ERROR_CODES.has(error.code)); +} diff --git a/packages/shared/src/routes/fiatCrypto.ts b/packages/shared/src/routes/fiatCrypto.ts index 1fa982ab63e8..981f06ba3c73 100644 --- a/packages/shared/src/routes/fiatCrypto.ts +++ b/packages/shared/src/routes/fiatCrypto.ts @@ -1,3 +1,4 @@ +import type { IFiatCryptoToken } from '@onekeyhq/shared/types/fiatCrypto'; import type { IAccountToken, ITokenFiat } from '@onekeyhq/shared/types/token'; import type { IDeriveTypesAddressParams } from './walletAddress'; @@ -5,6 +6,9 @@ import type { IDeriveTypesAddressParams } from './walletAddress'; export enum EModalFiatCryptoRoutes { BuyModal = 'Buy', DeriveTypesAddress = 'DeriveTypesAddress', + HeadlessBuy = 'HeadlessBuy', + HeadlessBuyTokenSelector = 'HeadlessBuyTokenSelector', + HeadlessBuySuccess = 'HeadlessBuySuccess', } export type IModalFiatCryptoParamList = { @@ -16,4 +20,38 @@ export type IModalFiatCryptoParamList = { defaultTab?: 'buy' | 'sell'; }; [EModalFiatCryptoRoutes.DeriveTypesAddress]: IDeriveTypesAddressParams; + // Buy-only by design — sell always stays on the web widget, so the page + // carries no `type` param. + [EModalFiatCryptoRoutes.HeadlessBuy]: { + networkId: string; + accountId?: string; + tokenAddress: string; + // Full token object when navigating from the token list; direct-buy entry + // points pass only tokenAddress and the page resolves the rest. + token?: IFiatCryptoToken; + }; + [EModalFiatCryptoRoutes.HeadlessBuyTokenSelector]: { + networkId: string; + accountId?: string; + // Invoked with the picked token before the selector pops itself. + // realAccountId may differ from accountId on merge-derive networks + // (address-type selection happens inside the list rows). + onSelected: (params: { + token: IFiatCryptoToken; + realAccountId?: string; + }) => void; + }; + // Display-only: the order is already submitted when this page mounts. It is + // reached via CommonActions.reset (the spent buy flow is wiped from the + // stack — the consumed native pay button must be unreachable via back). + [EModalFiatCryptoRoutes.HeadlessBuySuccess]: { + fiatAmount: number; + tokenSymbol: string; + networkId: string; + networkName?: string; + payout?: number; + providerName?: string; + address?: string; + checkoutId?: string; + }; }; diff --git a/packages/shared/src/routes/gallery.ts b/packages/shared/src/routes/gallery.ts index a41dc66f9907..87f3a6c5267a 100644 --- a/packages/shared/src/routes/gallery.ts +++ b/packages/shared/src/routes/gallery.ts @@ -9,6 +9,7 @@ export enum EGalleryRoutes { ComponentDiscoveryBrowser = 'component-DiscoveryBrowser', ComponentDotMap = 'component-DotMap', ComponentErrorToast = 'component-ErrorToast', + ComponentHeadlessBuy = 'component-HeadlessBuy', ComponentHardware = 'component-Hardware', ComponentLedgerAppOps = 'component-LedgerAppOps', ComponentImageCropGallery = 'component-ImageCrop', diff --git a/packages/shared/types/fiatCrypto.ts b/packages/shared/types/fiatCrypto.ts index 23172a6e1eef..78807eb006dc 100644 --- a/packages/shared/types/fiatCrypto.ts +++ b/packages/shared/types/fiatCrypto.ts @@ -23,6 +23,14 @@ export type IFiatCryptoToken = { balanceParsed?: string; fiatValue?: string; popular?: boolean; + // Whether the Onramper Headless SDK (native iOS Apple Pay checkout) can buy + // this token in the current region/fiat. Server-computed on the fiat-pay list. + headlessSupported?: boolean; + // Onramper asset id (e.g. 'usdt_ethereum') and network slug (e.g. + // 'ethereum') for the Headless checkout request — server-delivered on the + // fiat-pay list; the client keeps no local mapping. + onramperId?: string; + onramperNetworkCode?: string; }; export type IGetTokensListParams = { @@ -30,3 +38,12 @@ export type IGetTokensListParams = { type: IFiatCryptoType; accountId?: string; }; + +// Onramper Headless SDK session, minted by the OneKey backend (which SigV2-signs +// and forwards to Onramper partners/v2 client-sessions). The backend returns +// every field the SDK adapter's IOnramperSession may carry (extras like +// tokenFamilyId/expiresAt included) — deriving keeps the two shapes from +// drifting. +export type IOnramperSessionResponse = Required< + import('../src/modules3rdParty/onramper/type').IOnramperSession +>; diff --git a/patches/@onekeyfe+react-native-auto-size-input+3.0.78.patch b/patches/@onekeyfe+react-native-auto-size-input+3.0.78.patch deleted file mode 100644 index fe999a1c0f66..000000000000 --- a/patches/@onekeyfe+react-native-auto-size-input+3.0.78.patch +++ /dev/null @@ -1,857 +0,0 @@ -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/android/build.gradle b/node_modules/@onekeyfe/react-native-auto-size-input/android/build.gradle -index 55440cb..d32f581 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/android/build.gradle -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/android/build.gradle -@@ -125,4 +125,5 @@ dependencies { - implementation "com.facebook.react:react-android" - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation project(":react-native-nitro-modules") -+ testImplementation "junit:junit:4.13.2" - } -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/android/src/main/java/com/margelo/nitro/autosizeinput/AutoSizeInput.kt b/node_modules/@onekeyfe/react-native-auto-size-input/android/src/main/java/com/margelo/nitro/autosizeinput/AutoSizeInput.kt -index 369d3b4..60dec4a 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/android/src/main/java/com/margelo/nitro/autosizeinput/AutoSizeInput.kt -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/android/src/main/java/com/margelo/nitro/autosizeinput/AutoSizeInput.kt -@@ -8,7 +8,6 @@ import android.graphics.drawable.GradientDrawable - import android.os.Looper - import android.text.Editable - import android.text.InputType --import android.view.inputmethod.EditorInfo - import android.text.StaticLayout - import android.text.TextPaint - import android.text.TextWatcher -@@ -16,6 +15,7 @@ import android.util.TypedValue - import android.view.Gravity - import android.view.View - import android.view.ViewGroup -+import android.view.inputmethod.EditorInfo - import android.widget.EditText - import android.widget.TextView - import com.facebook.proguard.annotations.DoNotStrip -@@ -36,6 +36,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput - private var isDisposed = false - private var maxFontSizeProp: Double? = null - private var minFontSizeProp: Double? = null -+ private val textUpdateEventCounter = TextUpdateEventCounter() - - // Container view - override val view: View = object : ViewGroup(context) { -@@ -57,19 +58,28 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable?) { - if (isUpdatingFromJS || isDisposed) return -+ textUpdateEventCounter.recordNativeChange() - recalculateFontSize() - onChangeText?.invoke(s?.toString() ?: "") - } - } - - // Props -+ override var mostRecentEventCount: Double? -+ get() = textUpdateEventCounter.mostRecentEventCount.toDouble() -+ set(value) { -+ textUpdateEventCounter.mostRecentEventCount = value?.toInt() ?: 0 -+ } -+ - override var text: String? - get() = inputView.text?.toString() - set(value) { -- if (isDisposed) return -+ if (isDisposed || !textUpdateEventCounter.canApplyJsUpdate()) return -+ val nextText = value ?: "" -+ if (inputView.text?.toString() == nextText) return - isUpdatingFromJS = true - inputView.removeTextChangedListener(textWatcher) -- inputView.setText(value ?: "") -+ inputView.setText(nextText) - inputView.setSelection(inputView.text?.length ?: 0) - inputView.addTextChangedListener(textWatcher) - isUpdatingFromJS = false -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/android/src/main/java/com/margelo/nitro/autosizeinput/TextUpdateEventCounter.kt b/node_modules/@onekeyfe/react-native-auto-size-input/android/src/main/java/com/margelo/nitro/autosizeinput/TextUpdateEventCounter.kt -new file mode 100644 -index 0000000..fddf46b ---- /dev/null -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/android/src/main/java/com/margelo/nitro/autosizeinput/TextUpdateEventCounter.kt -@@ -0,0 +1,14 @@ -+package com.margelo.nitro.autosizeinput -+ -+internal class TextUpdateEventCounter { -+ var nativeEventCount: Int = 0 -+ private set -+ -+ var mostRecentEventCount: Int = 0 -+ -+ fun recordNativeChange() { -+ nativeEventCount += 1 -+ } -+ -+ fun canApplyJsUpdate(): Boolean = mostRecentEventCount >= nativeEventCount -+} -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/android/src/test/java/com/margelo/nitro/autosizeinput/TextUpdateEventCounterTest.kt b/node_modules/@onekeyfe/react-native-auto-size-input/android/src/test/java/com/margelo/nitro/autosizeinput/TextUpdateEventCounterTest.kt -new file mode 100644 -index 0000000..001f908 ---- /dev/null -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/android/src/test/java/com/margelo/nitro/autosizeinput/TextUpdateEventCounterTest.kt -@@ -0,0 +1,76 @@ -+package com.margelo.nitro.autosizeinput -+ -+import org.junit.Assert.assertEquals -+import org.junit.Assert.assertFalse -+import org.junit.Assert.assertTrue -+import org.junit.Test -+ -+class TextUpdateEventCounterTest { -+ @Test -+ fun allowsJsUpdatesBeforeNativeInput() { -+ val counter = TextUpdateEventCounter() -+ -+ assertTrue(counter.canApplyJsUpdate()) -+ } -+ -+ @Test -+ fun rejectsJsUpdatesThatAreOlderThanNativeInput() { -+ val counter = TextUpdateEventCounter() -+ counter.recordNativeChange() -+ counter.recordNativeChange() -+ counter.mostRecentEventCount = 1 -+ -+ assertFalse(counter.canApplyJsUpdate()) -+ } -+ -+ @Test -+ fun allowsTheLatestAcknowledgedJsUpdate() { -+ val counter = TextUpdateEventCounter() -+ counter.recordNativeChange() -+ counter.recordNativeChange() -+ counter.mostRecentEventCount = 2 -+ -+ assertTrue(counter.canApplyJsUpdate()) -+ } -+ -+ @Test -+ fun tracksEveryNativeTextChange() { -+ val counter = TextUpdateEventCounter() -+ -+ repeat(10) { -+ counter.recordNativeChange() -+ } -+ -+ assertEquals(10, counter.nativeEventCount) -+ } -+ -+ @Test -+ fun allowsAnAcknowledgedDeletionWithoutNewerInput() { -+ val counter = TextUpdateEventCounter() -+ repeat(4) { -+ counter.recordNativeChange() -+ } -+ counter.mostRecentEventCount = 4 -+ counter.recordNativeChange() -+ counter.mostRecentEventCount = 5 -+ -+ assertTrue(counter.canApplyJsUpdate()) -+ } -+ -+ @Test -+ fun rejectsAStaleDeletionUpdateAfterTheNextDigit() { -+ val counter = TextUpdateEventCounter() -+ repeat(4) { -+ counter.recordNativeChange() -+ } -+ counter.mostRecentEventCount = 4 -+ counter.recordNativeChange() -+ counter.recordNativeChange() -+ counter.mostRecentEventCount = 5 -+ -+ assertFalse(counter.canApplyJsUpdate()) -+ -+ counter.mostRecentEventCount = 6 -+ assertTrue(counter.canApplyJsUpdate()) -+ } -+} -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/ios/AutoSizeInput.swift b/node_modules/@onekeyfe/react-native-auto-size-input/ios/AutoSizeInput.swift -index 5823310..4911858 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/ios/AutoSizeInput.swift -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/ios/AutoSizeInput.swift -@@ -20,6 +20,8 @@ class HybridAutoSizeInput: HybridAutoSizeInputSpec { - var view: UIView = UIView() - - // MARK: - Props -+ var mostRecentEventCount: Double? -+ - var text: String? { - didSet { - guard !isUpdatingFromJS else { return } -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/.gitattributes b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/.gitattributes -new file mode 100644 -index 0000000..fb7a0d5 ---- /dev/null -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/.gitattributes -@@ -0,0 +1 @@ -+** linguist-generated=true -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.cpp b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.cpp -index b22b6d9..a635ddd 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.cpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.cpp -@@ -9,8 +9,8 @@ - - - --#include - #include -+#include - #include - #include "JFunc_void_std__string.hpp" - #include -@@ -45,6 +45,15 @@ namespace margelo::nitro::autosizeinput { - } - - // Properties -+ std::optional JHybridAutoSizeInputSpec::getMostRecentEventCount() { -+ static const auto method = javaClassStatic()->getMethod()>("getMostRecentEventCount"); -+ auto __result = method(_javaPart); -+ return __result != nullptr ? std::make_optional(__result->value()) : std::nullopt; -+ } -+ void JHybridAutoSizeInputSpec::setMostRecentEventCount(std::optional mostRecentEventCount) { -+ static const auto method = javaClassStatic()->getMethod /* mostRecentEventCount */)>("setMostRecentEventCount"); -+ method(_javaPart, mostRecentEventCount.has_value() ? jni::JDouble::valueOf(mostRecentEventCount.value()) : nullptr); -+ } - std::optional JHybridAutoSizeInputSpec::getText() { - static const auto method = javaClassStatic()->getMethod()>("getText"); - auto __result = method(_javaPart); -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.hpp b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.hpp -index 867c2a4..f9d7209 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.hpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.hpp -@@ -50,6 +50,8 @@ namespace margelo::nitro::autosizeinput { - - public: - // Properties -+ std::optional getMostRecentEventCount() override; -+ void setMostRecentEventCount(std::optional mostRecentEventCount) override; - std::optional getText() override; - void setText(const std::optional& text) override; - std::optional getPrefix() override; -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/c++/views/JHybridAutoSizeInputStateUpdater.cpp b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/c++/views/JHybridAutoSizeInputStateUpdater.cpp -index 79fca5c..ad963d4 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/c++/views/JHybridAutoSizeInputStateUpdater.cpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/c++/views/JHybridAutoSizeInputStateUpdater.cpp -@@ -36,6 +36,10 @@ void JHybridAutoSizeInputStateUpdater::updateViewProps(jni::alias_refsetMostRecentEventCount(props.mostRecentEventCount.value); -+ // TODO: Set isDirty = false -+ } - if (props.text.isDirty) { - view->setText(props.text.value); - // TODO: Set isDirty = false -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/autosizeinput/HybridAutoSizeInputSpec.kt b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/autosizeinput/HybridAutoSizeInputSpec.kt -index 43fd795..9262d57 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/autosizeinput/HybridAutoSizeInputSpec.kt -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/autosizeinput/HybridAutoSizeInputSpec.kt -@@ -42,6 +42,12 @@ abstract class HybridAutoSizeInputSpec: HybridView() { - } - - // Properties -+ @get:DoNotStrip -+ @get:Keep -+ @set:DoNotStrip -+ @set:Keep -+ abstract var mostRecentEventCount: Double? -+ - @get:DoNotStrip - @get:Keep - @set:DoNotStrip -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/AutoSizeInput-Swift-Cxx-Bridge.hpp b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/AutoSizeInput-Swift-Cxx-Bridge.hpp -index bd4953e..556d36b 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/AutoSizeInput-Swift-Cxx-Bridge.hpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/AutoSizeInput-Swift-Cxx-Bridge.hpp -@@ -30,33 +30,33 @@ namespace AutoSizeInput { class HybridAutoSizeInputSpec_cxx; } - */ - namespace margelo::nitro::autosizeinput::bridge::swift { - -- // pragma MARK: std::optional -+ // pragma MARK: std::optional - /** -- * Specialized version of `std::optional`. -+ * Specialized version of `std::optional`. - */ -- using std__optional_std__string_ = std::optional; -- inline std::optional create_std__optional_std__string_(const std::string& value) noexcept { -- return std::optional(value); -+ using std__optional_double_ = std::optional; -+ inline std::optional create_std__optional_double_(const double& value) noexcept { -+ return std::optional(value); - } -- inline bool has_value_std__optional_std__string_(const std::optional& optional) noexcept { -+ inline bool has_value_std__optional_double_(const std::optional& optional) noexcept { - return optional.has_value(); - } -- inline std::string get_std__optional_std__string_(const std::optional& optional) noexcept { -+ inline double get_std__optional_double_(const std::optional& optional) noexcept { - return *optional; - } - -- // pragma MARK: std::optional -+ // pragma MARK: std::optional - /** -- * Specialized version of `std::optional`. -+ * Specialized version of `std::optional`. - */ -- using std__optional_double_ = std::optional; -- inline std::optional create_std__optional_double_(const double& value) noexcept { -- return std::optional(value); -+ using std__optional_std__string_ = std::optional; -+ inline std::optional create_std__optional_std__string_(const std::string& value) noexcept { -+ return std::optional(value); - } -- inline bool has_value_std__optional_double_(const std::optional& optional) noexcept { -+ inline bool has_value_std__optional_std__string_(const std::optional& optional) noexcept { - return optional.has_value(); - } -- inline double get_std__optional_double_(const std::optional& optional) noexcept { -+ inline std::string get_std__optional_std__string_(const std::optional& optional) noexcept { - return *optional; - } - -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/c++/HybridAutoSizeInputSpecSwift.hpp b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/c++/HybridAutoSizeInputSpecSwift.hpp -index 11cd396..810d398 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/c++/HybridAutoSizeInputSpecSwift.hpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/c++/HybridAutoSizeInputSpecSwift.hpp -@@ -14,8 +14,8 @@ namespace AutoSizeInput { class HybridAutoSizeInputSpec_cxx; } - - - --#include - #include -+#include - #include - - #include "AutoSizeInput-Swift-Cxx-Umbrella.hpp" -@@ -58,6 +58,13 @@ namespace margelo::nitro::autosizeinput { - - public: - // Properties -+ inline std::optional getMostRecentEventCount() noexcept override { -+ auto __result = _swiftPart.getMostRecentEventCount(); -+ return __result; -+ } -+ inline void setMostRecentEventCount(std::optional mostRecentEventCount) noexcept override { -+ _swiftPart.setMostRecentEventCount(mostRecentEventCount); -+ } - inline std::optional getText() noexcept override { - auto __result = _swiftPart.getText(); - return __result; -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/c++/views/HybridAutoSizeInputComponent.mm b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/c++/views/HybridAutoSizeInputComponent.mm -index f4d6bd6..70790f0 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/c++/views/HybridAutoSizeInputComponent.mm -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/c++/views/HybridAutoSizeInputComponent.mm -@@ -71,6 +71,11 @@ - (void) updateProps:(const std::shared_ptr&)props - // 2. Update each prop individually - swiftPart.beforeUpdate(); - -+ // mostRecentEventCount: optional -+ if (newViewProps.mostRecentEventCount.isDirty) { -+ swiftPart.setMostRecentEventCount(newViewProps.mostRecentEventCount.value); -+ newViewProps.mostRecentEventCount.isDirty = false; -+ } - // text: optional - if (newViewProps.text.isDirty) { - swiftPart.setText(newViewProps.text.value); -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec.swift b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec.swift -index 43e1b8d..db8779a 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec.swift -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec.swift -@@ -11,6 +11,7 @@ import NitroModules - /// See ``HybridAutoSizeInputSpec`` - public protocol HybridAutoSizeInputSpec_protocol: HybridObject, HybridView { - // Properties -+ var mostRecentEventCount: Double? { get set } - var text: String? { get set } - var prefix: String? { get set } - var suffix: String? { get set } -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec_cxx.swift b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec_cxx.swift -index bbeec8d..f65ff49 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec_cxx.swift -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec_cxx.swift -@@ -114,6 +114,23 @@ open class HybridAutoSizeInputSpec_cxx { - } - - // Properties -+ public final var mostRecentEventCount: bridge.std__optional_double_ { -+ @inline(__always) -+ get { -+ return { () -> bridge.std__optional_double_ in -+ if let __unwrappedValue = self.__implementation.mostRecentEventCount { -+ return bridge.create_std__optional_double_(__unwrappedValue) -+ } else { -+ return .init() -+ } -+ }() -+ } -+ @inline(__always) -+ set { -+ self.__implementation.mostRecentEventCount = newValue.value -+ } -+ } -+ - public final var text: bridge.std__optional_std__string_ { - @inline(__always) - get { -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.cpp b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.cpp -index 5ca7b68..224cae9 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.cpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.cpp -@@ -14,6 +14,8 @@ namespace margelo::nitro::autosizeinput { - HybridObject::loadHybridMethods(); - // load custom methods/properties - registerHybrids(this, [](Prototype& prototype) { -+ prototype.registerHybridGetter("mostRecentEventCount", &HybridAutoSizeInputSpec::getMostRecentEventCount); -+ prototype.registerHybridSetter("mostRecentEventCount", &HybridAutoSizeInputSpec::setMostRecentEventCount); - prototype.registerHybridGetter("text", &HybridAutoSizeInputSpec::getText); - prototype.registerHybridSetter("text", &HybridAutoSizeInputSpec::setText); - prototype.registerHybridGetter("prefix", &HybridAutoSizeInputSpec::getPrefix); -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.hpp b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.hpp -index 4e29d93..8047afd 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.hpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.hpp -@@ -15,8 +15,8 @@ - - - --#include - #include -+#include - #include - - namespace margelo::nitro::autosizeinput { -@@ -46,6 +46,8 @@ namespace margelo::nitro::autosizeinput { - - public: - // Properties -+ virtual std::optional getMostRecentEventCount() = 0; -+ virtual void setMostRecentEventCount(std::optional mostRecentEventCount) = 0; - virtual std::optional getText() = 0; - virtual void setText(const std::optional& text) = 0; - virtual std::optional getPrefix() = 0; -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.cpp b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.cpp -index 21ad2e0..62ece82 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.cpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.cpp -@@ -25,6 +25,16 @@ namespace margelo::nitro::autosizeinput::views { - const HybridAutoSizeInputProps& sourceProps, - const react::RawProps& rawProps): - react::ViewProps(context, sourceProps, rawProps, filterObjectKeys), -+ mostRecentEventCount([&]() -> CachedProp> { -+ try { -+ const react::RawValue* rawValue = rawProps.at("mostRecentEventCount", nullptr, nullptr); -+ if (rawValue == nullptr) return sourceProps.mostRecentEventCount; -+ const auto& [runtime, value] = (std::pair)*rawValue; -+ return CachedProp>::fromRawValue(*runtime, value, sourceProps.mostRecentEventCount); -+ } catch (const std::exception& exc) { -+ throw std::runtime_error(std::string("AutoSizeInput.mostRecentEventCount: ") + exc.what()); -+ } -+ }()), - text([&]() -> CachedProp> { - try { - const react::RawValue* rawValue = rawProps.at("text", nullptr, nullptr); -@@ -338,6 +348,7 @@ namespace margelo::nitro::autosizeinput::views { - - HybridAutoSizeInputProps::HybridAutoSizeInputProps(const HybridAutoSizeInputProps& other): - react::ViewProps(), -+ mostRecentEventCount(other.mostRecentEventCount), - text(other.text), - prefix(other.prefix), - suffix(other.suffix), -@@ -372,6 +383,7 @@ namespace margelo::nitro::autosizeinput::views { - - bool HybridAutoSizeInputProps::filterObjectKeys(const std::string& propName) { - switch (hashString(propName)) { -+ case hashString("mostRecentEventCount"): return true; - case hashString("text"): return true; - case hashString("prefix"): return true; - case hashString("suffix"): return true; -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.hpp b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.hpp -index e4c8f15..8bb10a9 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.hpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.hpp -@@ -16,8 +16,8 @@ - #include - #include - --#include - #include -+#include - #include - #include - #include "HybridAutoSizeInputSpec.hpp" -@@ -43,6 +43,7 @@ namespace margelo::nitro::autosizeinput::views { - const react::RawProps& rawProps); - - public: -+ CachedProp> mostRecentEventCount; - CachedProp> text; - CachedProp> prefix; - CachedProp> suffix; -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/json/AutoSizeInputConfig.json b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/json/AutoSizeInputConfig.json -index 261adfa..b8264e0 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/json/AutoSizeInputConfig.json -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/nitrogen/generated/shared/json/AutoSizeInputConfig.json -@@ -4,6 +4,7 @@ - "bubblingEventTypes": {}, - "directEventTypes": {}, - "validAttributes": { -+ "mostRecentEventCount": true, - "text": true, - "prefix": true, - "suffix": true, -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/lib/typescript/src/AutoSizeInput.nitro.d.ts b/node_modules/@onekeyfe/react-native-auto-size-input/lib/typescript/src/AutoSizeInput.nitro.d.ts -index 097266e..6e4f149 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/lib/typescript/src/AutoSizeInput.nitro.d.ts -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/lib/typescript/src/AutoSizeInput.nitro.d.ts -@@ -1,5 +1,6 @@ - import type { HybridView, HybridViewMethods, HybridViewProps } from 'react-native-nitro-modules'; - export interface AutoSizeInputProps extends HybridViewProps { -+ mostRecentEventCount?: number; - text?: string; - prefix?: string; - suffix?: string; -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/.gitattributes b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/.gitattributes -new file mode 100644 -index 0000000..fb7a0d5 ---- /dev/null -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/.gitattributes -@@ -0,0 +1 @@ -+** linguist-generated=true -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.cpp b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.cpp -index b22b6d9..a635ddd 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.cpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.cpp -@@ -9,8 +9,8 @@ - - - --#include - #include -+#include - #include - #include "JFunc_void_std__string.hpp" - #include -@@ -45,6 +45,15 @@ namespace margelo::nitro::autosizeinput { - } - - // Properties -+ std::optional JHybridAutoSizeInputSpec::getMostRecentEventCount() { -+ static const auto method = javaClassStatic()->getMethod()>("getMostRecentEventCount"); -+ auto __result = method(_javaPart); -+ return __result != nullptr ? std::make_optional(__result->value()) : std::nullopt; -+ } -+ void JHybridAutoSizeInputSpec::setMostRecentEventCount(std::optional mostRecentEventCount) { -+ static const auto method = javaClassStatic()->getMethod /* mostRecentEventCount */)>("setMostRecentEventCount"); -+ method(_javaPart, mostRecentEventCount.has_value() ? jni::JDouble::valueOf(mostRecentEventCount.value()) : nullptr); -+ } - std::optional JHybridAutoSizeInputSpec::getText() { - static const auto method = javaClassStatic()->getMethod()>("getText"); - auto __result = method(_javaPart); -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.hpp b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.hpp -index 867c2a4..f9d7209 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.hpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.hpp -@@ -50,6 +50,8 @@ namespace margelo::nitro::autosizeinput { - - public: - // Properties -+ std::optional getMostRecentEventCount() override; -+ void setMostRecentEventCount(std::optional mostRecentEventCount) override; - std::optional getText() override; - void setText(const std::optional& text) override; - std::optional getPrefix() override; -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/c++/views/JHybridAutoSizeInputStateUpdater.cpp b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/c++/views/JHybridAutoSizeInputStateUpdater.cpp -index 79fca5c..ad963d4 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/c++/views/JHybridAutoSizeInputStateUpdater.cpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/c++/views/JHybridAutoSizeInputStateUpdater.cpp -@@ -36,6 +36,10 @@ void JHybridAutoSizeInputStateUpdater::updateViewProps(jni::alias_refsetMostRecentEventCount(props.mostRecentEventCount.value); -+ // TODO: Set isDirty = false -+ } - if (props.text.isDirty) { - view->setText(props.text.value); - // TODO: Set isDirty = false -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/kotlin/com/margelo/nitro/autosizeinput/HybridAutoSizeInputSpec.kt b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/kotlin/com/margelo/nitro/autosizeinput/HybridAutoSizeInputSpec.kt -index 43fd795..9262d57 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/kotlin/com/margelo/nitro/autosizeinput/HybridAutoSizeInputSpec.kt -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/android/kotlin/com/margelo/nitro/autosizeinput/HybridAutoSizeInputSpec.kt -@@ -42,6 +42,12 @@ abstract class HybridAutoSizeInputSpec: HybridView() { - } - - // Properties -+ @get:DoNotStrip -+ @get:Keep -+ @set:DoNotStrip -+ @set:Keep -+ abstract var mostRecentEventCount: Double? -+ - @get:DoNotStrip - @get:Keep - @set:DoNotStrip -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/AutoSizeInput-Swift-Cxx-Bridge.hpp b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/AutoSizeInput-Swift-Cxx-Bridge.hpp -index bd4953e..556d36b 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/AutoSizeInput-Swift-Cxx-Bridge.hpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/AutoSizeInput-Swift-Cxx-Bridge.hpp -@@ -30,33 +30,33 @@ namespace AutoSizeInput { class HybridAutoSizeInputSpec_cxx; } - */ - namespace margelo::nitro::autosizeinput::bridge::swift { - -- // pragma MARK: std::optional -+ // pragma MARK: std::optional - /** -- * Specialized version of `std::optional`. -+ * Specialized version of `std::optional`. - */ -- using std__optional_std__string_ = std::optional; -- inline std::optional create_std__optional_std__string_(const std::string& value) noexcept { -- return std::optional(value); -+ using std__optional_double_ = std::optional; -+ inline std::optional create_std__optional_double_(const double& value) noexcept { -+ return std::optional(value); - } -- inline bool has_value_std__optional_std__string_(const std::optional& optional) noexcept { -+ inline bool has_value_std__optional_double_(const std::optional& optional) noexcept { - return optional.has_value(); - } -- inline std::string get_std__optional_std__string_(const std::optional& optional) noexcept { -+ inline double get_std__optional_double_(const std::optional& optional) noexcept { - return *optional; - } - -- // pragma MARK: std::optional -+ // pragma MARK: std::optional - /** -- * Specialized version of `std::optional`. -+ * Specialized version of `std::optional`. - */ -- using std__optional_double_ = std::optional; -- inline std::optional create_std__optional_double_(const double& value) noexcept { -- return std::optional(value); -+ using std__optional_std__string_ = std::optional; -+ inline std::optional create_std__optional_std__string_(const std::string& value) noexcept { -+ return std::optional(value); - } -- inline bool has_value_std__optional_double_(const std::optional& optional) noexcept { -+ inline bool has_value_std__optional_std__string_(const std::optional& optional) noexcept { - return optional.has_value(); - } -- inline double get_std__optional_double_(const std::optional& optional) noexcept { -+ inline std::string get_std__optional_std__string_(const std::optional& optional) noexcept { - return *optional; - } - -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/c++/HybridAutoSizeInputSpecSwift.hpp b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/c++/HybridAutoSizeInputSpecSwift.hpp -index 11cd396..810d398 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/c++/HybridAutoSizeInputSpecSwift.hpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/c++/HybridAutoSizeInputSpecSwift.hpp -@@ -14,8 +14,8 @@ namespace AutoSizeInput { class HybridAutoSizeInputSpec_cxx; } - - - --#include - #include -+#include - #include - - #include "AutoSizeInput-Swift-Cxx-Umbrella.hpp" -@@ -58,6 +58,13 @@ namespace margelo::nitro::autosizeinput { - - public: - // Properties -+ inline std::optional getMostRecentEventCount() noexcept override { -+ auto __result = _swiftPart.getMostRecentEventCount(); -+ return __result; -+ } -+ inline void setMostRecentEventCount(std::optional mostRecentEventCount) noexcept override { -+ _swiftPart.setMostRecentEventCount(mostRecentEventCount); -+ } - inline std::optional getText() noexcept override { - auto __result = _swiftPart.getText(); - return __result; -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/c++/views/HybridAutoSizeInputComponent.mm b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/c++/views/HybridAutoSizeInputComponent.mm -index f4d6bd6..70790f0 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/c++/views/HybridAutoSizeInputComponent.mm -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/c++/views/HybridAutoSizeInputComponent.mm -@@ -71,6 +71,11 @@ - (void) updateProps:(const std::shared_ptr&)props - // 2. Update each prop individually - swiftPart.beforeUpdate(); - -+ // mostRecentEventCount: optional -+ if (newViewProps.mostRecentEventCount.isDirty) { -+ swiftPart.setMostRecentEventCount(newViewProps.mostRecentEventCount.value); -+ newViewProps.mostRecentEventCount.isDirty = false; -+ } - // text: optional - if (newViewProps.text.isDirty) { - swiftPart.setText(newViewProps.text.value); -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec.swift b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec.swift -index 43e1b8d..db8779a 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec.swift -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec.swift -@@ -11,6 +11,7 @@ import NitroModules - /// See ``HybridAutoSizeInputSpec`` - public protocol HybridAutoSizeInputSpec_protocol: HybridObject, HybridView { - // Properties -+ var mostRecentEventCount: Double? { get set } - var text: String? { get set } - var prefix: String? { get set } - var suffix: String? { get set } -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec_cxx.swift b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec_cxx.swift -index bbeec8d..f65ff49 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec_cxx.swift -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec_cxx.swift -@@ -114,6 +114,23 @@ open class HybridAutoSizeInputSpec_cxx { - } - - // Properties -+ public final var mostRecentEventCount: bridge.std__optional_double_ { -+ @inline(__always) -+ get { -+ return { () -> bridge.std__optional_double_ in -+ if let __unwrappedValue = self.__implementation.mostRecentEventCount { -+ return bridge.create_std__optional_double_(__unwrappedValue) -+ } else { -+ return .init() -+ } -+ }() -+ } -+ @inline(__always) -+ set { -+ self.__implementation.mostRecentEventCount = newValue.value -+ } -+ } -+ - public final var text: bridge.std__optional_std__string_ { - @inline(__always) - get { -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.cpp b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.cpp -index 5ca7b68..224cae9 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.cpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.cpp -@@ -14,6 +14,8 @@ namespace margelo::nitro::autosizeinput { - HybridObject::loadHybridMethods(); - // load custom methods/properties - registerHybrids(this, [](Prototype& prototype) { -+ prototype.registerHybridGetter("mostRecentEventCount", &HybridAutoSizeInputSpec::getMostRecentEventCount); -+ prototype.registerHybridSetter("mostRecentEventCount", &HybridAutoSizeInputSpec::setMostRecentEventCount); - prototype.registerHybridGetter("text", &HybridAutoSizeInputSpec::getText); - prototype.registerHybridSetter("text", &HybridAutoSizeInputSpec::setText); - prototype.registerHybridGetter("prefix", &HybridAutoSizeInputSpec::getPrefix); -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.hpp b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.hpp -index 4e29d93..8047afd 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.hpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.hpp -@@ -15,8 +15,8 @@ - - - --#include - #include -+#include - #include - - namespace margelo::nitro::autosizeinput { -@@ -46,6 +46,8 @@ namespace margelo::nitro::autosizeinput { - - public: - // Properties -+ virtual std::optional getMostRecentEventCount() = 0; -+ virtual void setMostRecentEventCount(std::optional mostRecentEventCount) = 0; - virtual std::optional getText() = 0; - virtual void setText(const std::optional& text) = 0; - virtual std::optional getPrefix() = 0; -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.cpp b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.cpp -index 21ad2e0..62ece82 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.cpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.cpp -@@ -25,6 +25,16 @@ namespace margelo::nitro::autosizeinput::views { - const HybridAutoSizeInputProps& sourceProps, - const react::RawProps& rawProps): - react::ViewProps(context, sourceProps, rawProps, filterObjectKeys), -+ mostRecentEventCount([&]() -> CachedProp> { -+ try { -+ const react::RawValue* rawValue = rawProps.at("mostRecentEventCount", nullptr, nullptr); -+ if (rawValue == nullptr) return sourceProps.mostRecentEventCount; -+ const auto& [runtime, value] = (std::pair)*rawValue; -+ return CachedProp>::fromRawValue(*runtime, value, sourceProps.mostRecentEventCount); -+ } catch (const std::exception& exc) { -+ throw std::runtime_error(std::string("AutoSizeInput.mostRecentEventCount: ") + exc.what()); -+ } -+ }()), - text([&]() -> CachedProp> { - try { - const react::RawValue* rawValue = rawProps.at("text", nullptr, nullptr); -@@ -338,6 +348,7 @@ namespace margelo::nitro::autosizeinput::views { - - HybridAutoSizeInputProps::HybridAutoSizeInputProps(const HybridAutoSizeInputProps& other): - react::ViewProps(), -+ mostRecentEventCount(other.mostRecentEventCount), - text(other.text), - prefix(other.prefix), - suffix(other.suffix), -@@ -372,6 +383,7 @@ namespace margelo::nitro::autosizeinput::views { - - bool HybridAutoSizeInputProps::filterObjectKeys(const std::string& propName) { - switch (hashString(propName)) { -+ case hashString("mostRecentEventCount"): return true; - case hashString("text"): return true; - case hashString("prefix"): return true; - case hashString("suffix"): return true; -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.hpp b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.hpp -index e4c8f15..8bb10a9 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.hpp -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.hpp -@@ -16,8 +16,8 @@ - #include - #include - --#include - #include -+#include - #include - #include - #include "HybridAutoSizeInputSpec.hpp" -@@ -43,6 +43,7 @@ namespace margelo::nitro::autosizeinput::views { - const react::RawProps& rawProps); - - public: -+ CachedProp> mostRecentEventCount; - CachedProp> text; - CachedProp> prefix; - CachedProp> suffix; -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/json/AutoSizeInputConfig.json b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/json/AutoSizeInputConfig.json -index 261adfa..b8264e0 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/json/AutoSizeInputConfig.json -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/nitrogen/generated/shared/json/AutoSizeInputConfig.json -@@ -4,6 +4,7 @@ - "bubblingEventTypes": {}, - "directEventTypes": {}, - "validAttributes": { -+ "mostRecentEventCount": true, - "text": true, - "prefix": true, - "suffix": true, -diff --git a/node_modules/@onekeyfe/react-native-auto-size-input/src/AutoSizeInput.nitro.ts b/node_modules/@onekeyfe/react-native-auto-size-input/src/AutoSizeInput.nitro.ts -index f1e93c0..f4441b2 100644 ---- a/node_modules/@onekeyfe/react-native-auto-size-input/src/AutoSizeInput.nitro.ts -+++ b/node_modules/@onekeyfe/react-native-auto-size-input/src/AutoSizeInput.nitro.ts -@@ -6,6 +6,7 @@ import type { - - export interface AutoSizeInputProps extends HybridViewProps { - // Text content -+ mostRecentEventCount?: number; - text?: string; - prefix?: string; - suffix?: string; diff --git a/patches/react-native-nitro-modules+0.33.2.patch b/patches/react-native-nitro-modules+0.35.2.patch similarity index 96% rename from patches/react-native-nitro-modules+0.33.2.patch rename to patches/react-native-nitro-modules+0.35.2.patch index 778ef2888b10..76d50af7fc06 100644 --- a/patches/react-native-nitro-modules+0.33.2.patch +++ b/patches/react-native-nitro-modules+0.35.2.patch @@ -92,7 +92,7 @@ index 7578579..e71b494 100644 private: static constexpr auto TAG = "JSICache"; diff --git a/node_modules/react-native-nitro-modules/cpp/prototype/HybridObjectPrototype.cpp b/node_modules/react-native-nitro-modules/cpp/prototype/HybridObjectPrototype.cpp -index c4fc0f6..33d418d 100644 +index 62ab82a..0b88ad2 100644 --- a/node_modules/react-native-nitro-modules/cpp/prototype/HybridObjectPrototype.cpp +++ b/node_modules/react-native-nitro-modules/cpp/prototype/HybridObjectPrototype.cpp @@ -13,7 +13,8 @@ @@ -154,7 +154,7 @@ index c4fc0f6..33d418d 100644 // 2. Get the base prototype of this prototype (recursively), then create the individual prototypes downwards // until we reach our current prototype. -@@ -93,13 +118,30 @@ jsi::Value HybridObjectPrototype::createPrototype(jsi::Runtime& runtime, const s +@@ -93,16 +118,33 @@ jsi::Value HybridObjectPrototype::createPrototype(jsi::Runtime& runtime, const s CommonGlobals::Object::freeze(runtime, object); #endif @@ -162,8 +162,11 @@ index c4fc0f6..33d418d 100644 + // 8. Throw it into our cache so the next lookup can be cached and therefore faster. + // `JSICache::getOrCreateCache(...)` and `makeShared(...)` run outside the lock (they re-enter + // the cache machinery on this thread); only the write into the inner map is locked. - JSICacheReference jsiCache = JSICache::getOrCreateCache(runtime); - BorrowingReference sharedObject = jsiCache.makeShared(std::move(object)); + BorrowingReference sharedObject; + { + JSICacheReference jsiCache = JSICache::getOrCreateCache(runtime); + sharedObject = jsiCache.makeShared(std::move(object)); + } const NativeInstanceId& instanceId = prototype->getNativeInstanceId(); - prototypeCache[instanceId] = sharedObject; + BorrowingReference existingObject; @@ -218,7 +221,7 @@ index 522f991..4e7b133 100644 protected: /** diff --git a/node_modules/react-native-nitro-modules/cpp/prototype/Prototype.hpp b/node_modules/react-native-nitro-modules/cpp/prototype/Prototype.hpp -index 7019337..d1ff55d 100644 +index 4bc81f6..98cbd1d 100644 --- a/node_modules/react-native-nitro-modules/cpp/prototype/Prototype.hpp +++ b/node_modules/react-native-nitro-modules/cpp/prototype/Prototype.hpp @@ -9,6 +9,7 @@ @@ -242,10 +245,10 @@ index 7019337..d1ff55d 100644 + static std::mutex mutex; + std::lock_guard lock(mutex); - const auto& found = _prototypesCache.find(typeId); - if (found != _prototypesCache.end()) { + auto [it, inserted] = _prototypesCache.try_emplace(typeId); + if (inserted) { diff --git a/node_modules/react-native-nitro-modules/cpp/threading/Dispatcher.cpp b/node_modules/react-native-nitro-modules/cpp/threading/Dispatcher.cpp -index 00ba8f4..b027abe 100644 +index 84fc2bb..7580b3e 100644 --- a/node_modules/react-native-nitro-modules/cpp/threading/Dispatcher.cpp +++ b/node_modules/react-native-nitro-modules/cpp/threading/Dispatcher.cpp @@ -11,6 +11,7 @@ @@ -308,7 +311,7 @@ index 00ba8f4..b027abe 100644 std::shared_ptr strongDispatcher = weakDispatcher.lock(); if (strongDispatcher) { // the weak reference we cached is still valid - return it! -@@ -47,12 +69,16 @@ std::shared_ptr Dispatcher::getRuntimeGlobalDispatcher(jsi::Runtime& +@@ -53,12 +75,16 @@ std::shared_ptr Dispatcher::getRuntimeGlobalDispatcher(jsi::Runtime& Logger::log(LogLevel::Warning, TAG, "Unknown Runtime (%s), looking for Dispatcher through JSI global lookup...", getRuntimeId(runtime).c_str()); @@ -353,7 +356,7 @@ index dfe3070..b8dee5f 100644 private: static constexpr auto TAG = "Dispatcher"; diff --git a/node_modules/react-native-nitro-modules/cpp/utils/CommonGlobals.cpp b/node_modules/react-native-nitro-modules/cpp/utils/CommonGlobals.cpp -index 020a884..b300e94 100644 +index 126fe49..3476399 100644 --- a/node_modules/react-native-nitro-modules/cpp/utils/CommonGlobals.cpp +++ b/node_modules/react-native-nitro-modules/cpp/utils/CommonGlobals.cpp @@ -22,7 +22,8 @@ namespace margelo::nitro { @@ -366,7 +369,7 @@ index 020a884..b300e94 100644 // pragma MARK: Object -@@ -187,26 +188,55 @@ const jsi::PropNameID& CommonGlobals::getKnownGlobalPropertyName(jsi::Runtime& r +@@ -187,20 +188,40 @@ const jsi::PropNameID& CommonGlobals::getKnownGlobalPropertyName(jsi::Runtime& r const jsi::Function& CommonGlobals::getGlobalFunction(jsi::Runtime& runtime, const char* key, std::function getFunction) { @@ -392,11 +395,10 @@ index 020a884..b300e94 100644 + auto& slot = _cache[&runtime]; + if (!slot) { + slot = std::make_shared(); - } ++ } + entry = slot; - } - -- // We haven't found the function with the given key in cache - so let's get it: ++ } ++ + // Let's try to find the function in cache under the entry's own lock. + { + std::lock_guard elk(entry->mutex); @@ -409,16 +411,19 @@ index 020a884..b300e94 100644 + // heap (stable address), so it is safe to return the reference after releasing the lock. + return *function; + } -+ } -+ } -+ + } + } + +- // We haven't found the function with the given key in cache - so let's get it: + // We haven't found the function with the given key in cache - so let's get it. This MUST happen + // outside any of our locks because `JSICache::getOrCreateCache(...)` can re-enter this function. jsi::Function function = getFunction(runtime); // Let's throw it in cache! - JSICacheReference jsiCache = JSICache::getOrCreateCache(runtime); - BorrowingReference sharedFunction = jsiCache.makeShared(std::move(function)); +@@ -209,7 +230,16 @@ const jsi::Function& CommonGlobals::getGlobalFunction(jsi::Runtime& runtime, con + JSICacheReference jsiCache = JSICache::getOrCreateCache(runtime); + sharedFunction = jsiCache.makeShared(std::move(function)); + } - functionCache[stringKey] = sharedFunction; + + // Store it (double-checked: another thread may have inserted it while we were unlocked). @@ -471,7 +476,7 @@ index 388e74b..03eb9a9 100644 } // namespace margelo::nitro diff --git a/node_modules/react-native-nitro-modules/cpp/utils/PropNameIDCache.cpp b/node_modules/react-native-nitro-modules/cpp/utils/PropNameIDCache.cpp -index 5c83355..fc13fc9 100644 +index e238bd7..fc13fc9 100644 --- a/node_modules/react-native-nitro-modules/cpp/utils/PropNameIDCache.cpp +++ b/node_modules/react-native-nitro-modules/cpp/utils/PropNameIDCache.cpp @@ -12,28 +12,57 @@ namespace margelo::nitro { @@ -528,7 +533,7 @@ index 5c83355..fc13fc9 100644 auto sharedPropName = jsiCache.makeShared(std::move(propName)); - // store it in cache... -- cache.emplace(value, sharedPropName); +- cache.insert_or_assign(value, sharedPropName); + // Store it in cache (double-checked: another thread may have inserted it while we were unlocked). + { + std::lock_guard elk(entry->mutex); diff --git a/yarn.lock b/yarn.lock index b1f5f23edd04..9239a7838a94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9552,25 +9552,25 @@ __metadata: languageName: node linkType: hard -"@onekeyfe/react-native-app-update@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-app-update@npm:3.0.78" +"@onekeyfe/react-native-app-update@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-app-update@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/fbf9f42e53d9dc6a85c660c9631ed13c1abaff3930f3327189b4e703ce2b89088f283c2f2c793f5510c5681ce24f08c4fd7aedcd3389487d17fbd805d94bbedc + react-native-nitro-modules: 0.35.2 + checksum: 10/11b0094034357729bbcd6071ee9b4322a445db4007ec910188cd1554d31dd0ec6f09f17620ca6de8a7c3ff04bdda56c56b162b5e7dd982f306bb7ef73f0a4293 languageName: node linkType: hard -"@onekeyfe/react-native-auto-size-input@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-auto-size-input@npm:3.0.78" +"@onekeyfe/react-native-auto-size-input@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-auto-size-input@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/ae660624f3b168a90bd75b764ef4fd39ae5712278a05b9ae9510a313ea1f422c1c099c2860d7bd1f4b3e4c3bfad256caeec0f19c349ed16445bd94aeafea348e + react-native-nitro-modules: 0.35.2 + checksum: 10/474e1213b71dcbd6f08745f5308eb07c308cf807f467951cf51ccf4132a3592dfa220e7a5367ec9f48e8cbd59c51b70998637861f7adfb696a0b7c4bd55c24bd languageName: node linkType: hard @@ -9595,80 +9595,80 @@ __metadata: languageName: node linkType: hard -"@onekeyfe/react-native-bundle-crypto@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-bundle-crypto@npm:3.0.78" +"@onekeyfe/react-native-bundle-crypto@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-bundle-crypto@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/28776df4b0704188dce9c1de9dba666b404c194e745bdab1f02a2c24df6a003ae00382953db599b51f5bdbe0619404d5413c34c0eb155ed40996d71c3d0856fe + react-native-nitro-modules: 0.35.2 + checksum: 10/cd9457530b3688c67e610d9b3450c6a29c56d304643dded4ac920796dc3b0e2cfb69cc9500b2e8fef377c44f13c206d91166a0ec908eaaa25e3d48e3eddd3c80 languageName: node linkType: hard -"@onekeyfe/react-native-bundle-update@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-bundle-update@npm:3.0.78" +"@onekeyfe/react-native-bundle-update@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-bundle-update@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/f6245ba19cac35809cf0e144ff9283909517c92593436f3454dbcf9a5fd32252a0e785b5a09112bb15d1a108cc577e5e381bca3da9b9194e72058727627b4e1f + react-native-nitro-modules: 0.35.2 + checksum: 10/03c4a554d442023b3470237b2bf3128172c8c96ab12a5caf62f84d401f52ff1241878d979beb2d8f92a89365444cacd9f904315d1f741d1875a37b80a4e27b5c languageName: node linkType: hard -"@onekeyfe/react-native-chart-webview@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-chart-webview@npm:3.0.78" +"@onekeyfe/react-native-chart-webview@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-chart-webview@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/555af52d3cca76723577be1b23568f976fe8a6cb6d0b22c0aaac9d576de1052d05852205d47a52d5d7507353bd99803d51e06fcfc1c5a229e8a98e09c376c2ce + react-native-nitro-modules: 0.35.2 + checksum: 10/6639162b5479c3a04024996bdb8a38914e5adf471ec1d40353dcbfa2228e0ffc1404330e19f05ff762f418f30cf70e8ef1b99810a68f3d0e54272f38c0924af5 languageName: node linkType: hard -"@onekeyfe/react-native-check-biometric-auth-changed@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-check-biometric-auth-changed@npm:3.0.78" +"@onekeyfe/react-native-check-biometric-auth-changed@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-check-biometric-auth-changed@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/1739b313f8586d3dcb987046a6f197e93133511f56bf1ba56db9519cc2f38a0271bc3d72bc191cf86be43b746bfc2180561439876a5893b0b39b6021563f9ccc + react-native-nitro-modules: 0.35.2 + checksum: 10/f5b2f4742902e3de7d4fe35119f1b860e1bb161dcbfe62b0a455e44873e5c4b8a5a2ab85d7b3cdd27ae0c42701e0443829b198508a6423c6dc532f5012a3dfae languageName: node linkType: hard -"@onekeyfe/react-native-cloud-kit-module@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-cloud-kit-module@npm:3.0.78" +"@onekeyfe/react-native-cloud-kit-module@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-cloud-kit-module@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/53fbc454176ddb7fbbdf8a0c36985bad4c503f04c1111fe49b9e9d5ace6b07beebebe8041267e014d0db6f97e7e3588e1cdb181fb2334d330fd900e68b2e2372 + react-native-nitro-modules: 0.35.2 + checksum: 10/5a59af4207e2a5f9237ac92c70be80694c8caa7af6c85130785441cc86fe5d7af12dcf18e116bd38e722e3ab608f5f6b6c41cbdf2d0b273b7bd77fa6ff9cc81e languageName: node linkType: hard -"@onekeyfe/react-native-device-utils@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-device-utils@npm:3.0.78" +"@onekeyfe/react-native-device-utils@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-device-utils@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/ba21d0f2f92e1e37aa6f3a0c3e827fcfaf1616c263b12d5fc5103ef6ce624cedab95ee20849260b11a5232611b570e42836b54e669093f357ee333ef3debc185 + react-native-nitro-modules: 0.35.2 + checksum: 10/09a67535827f9aa833fc0d629daa6062f6d7809c70788f9510169e964cd04f2678a1b99f5f492cde0645ce94a22ef5ff85cff0207ce52a088fc27d0b1ea4e7f7 languageName: node linkType: hard -"@onekeyfe/react-native-keychain-module@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-keychain-module@npm:3.0.78" +"@onekeyfe/react-native-keychain-module@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-keychain-module@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/4831f313eb5fc9b4566a55d896fb918daff76fa543cc806e32d9d7ac4c4e291457245591439d867ef4ca4146aefa2ac6a04760ffd79f1fc8af69d7ccfc5ef810 + react-native-nitro-modules: 0.35.2 + checksum: 10/6f8c467d9904de7abdf76fbe91207b6250c83e89108517f627e04cbe1667f2bcb39edcb95bb88c8604f3a1ceb3784e999b7d17ab2d20c8f309aa17bf6feead91 languageName: node linkType: hard @@ -9682,14 +9682,14 @@ __metadata: languageName: node linkType: hard -"@onekeyfe/react-native-native-logger@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-native-logger@npm:3.0.78" +"@onekeyfe/react-native-native-logger@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-native-logger@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/cb9faaa167fbac3a3b0cfb018848de45a7b036fb1db9c00c8b89749c43374b052ac995548ad7afe2666c514c243de83153eb6529ce214bb8b6e5a02b7dce836c + react-native-nitro-modules: 0.35.2 + checksum: 10/aaf85d9cfc2d552941d8dd7698a9ca9ece9324d16352d9dbfb88fbe76b755582c436a8b3d20c82ace8253f6a370a470f41da3f6dca4ae78418d8f92c52c88909 languageName: node linkType: hard @@ -9703,47 +9703,47 @@ __metadata: languageName: node linkType: hard -"@onekeyfe/react-native-perf-memory@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-perf-memory@npm:3.0.78" +"@onekeyfe/react-native-perf-memory@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-perf-memory@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/595cf86ad63d3517557d380fe857f755983ac18f867fea4f8ce5d0568264eac5982f707be09778125b2a3c3e2b95700059acb531220dd3333f32d178cdac7e32 + react-native-nitro-modules: 0.35.2 + checksum: 10/0c5eeb4f78a358a3ed14cbfc054892123679a0efade0c2649441104437d0230ae6ae9ed1f524d0fe8718787ae353d93b1dfc9c32268e11f70a6546f40c071e2e languageName: node linkType: hard -"@onekeyfe/react-native-perf-stats@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-perf-stats@npm:3.0.78" +"@onekeyfe/react-native-perf-stats@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-perf-stats@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/0e6b64d45ab8bbea7d0279e9b92fe2c2aaad6e652cf671668ad22e2e9bd441382250995bf34b5adb9e4766204395151a226886ae87c2af8ae87e150deca9f600 + react-native-nitro-modules: 0.35.2 + checksum: 10/ba7ab64fe928bd0185694a0146f21560d515c0275a4f8833707d6f918e928522f9dcdf358d644f220207abfb14335d38370123591e8bd6cf1271e312e4c091a7 languageName: node linkType: hard -"@onekeyfe/react-native-perp-depth-bar@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-perp-depth-bar@npm:3.0.78" +"@onekeyfe/react-native-perp-depth-bar@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-perp-depth-bar@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/0f75bc4e57e9ab150c59b6413423ded8e5ec32f792e57b3362d2221f456a2bbf103a8075587b735e168b48a57831073f184348cd38e4ea86c618e8fa32f6f636 + react-native-nitro-modules: 0.35.2 + checksum: 10/9f727fa090ec249fe1c13a6010d1aa159f833d40f6b68a59edc629598c2f15cb1a7164f1de7a5bf8049b7dc59316d57248aba3a9f68fd46e04b106837e10d211 languageName: node linkType: hard -"@onekeyfe/react-native-range-downloader@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-range-downloader@npm:3.0.78" +"@onekeyfe/react-native-range-downloader@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-range-downloader@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/7019df4b24e2783a054dc2b6cafa429f07a308b098603e6ef4a38fc0882541508438177be072f67879dfe960d2802d78b3c8d84607dc5e749655e66509d6433e + react-native-nitro-modules: 0.35.2 + checksum: 10/e1ae8eeae404cf2f0bb83e5a422804ab30c3e36641dda46cdae225d3ccecd5e00aaef431f9b9fbad9a2d22113c3c84ee84b38189e050f07ffcd2621a2c47070f languageName: node linkType: hard @@ -9758,6 +9758,17 @@ __metadata: languageName: node linkType: hard +"@onekeyfe/react-native-scroll-guard@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-scroll-guard@npm:3.0.82" + peerDependencies: + react: "*" + react-native: "*" + react-native-nitro-modules: 0.35.2 + checksum: 10/1a9cafd2646d8da390940351dbd49879c67c2b75c94fe4b36be2128612a3b47723e8889e7a6722621f2d7ab1816780f7a0f23fa0fb29ab5ce7bf371a4f56799f + languageName: node + linkType: hard + "@onekeyfe/react-native-segment-slider@npm:3.0.78": version: 3.0.78 resolution: "@onekeyfe/react-native-segment-slider@npm:3.0.78" @@ -9769,14 +9780,25 @@ __metadata: languageName: node linkType: hard -"@onekeyfe/react-native-skeleton@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-skeleton@npm:3.0.78" +"@onekeyfe/react-native-segment-slider@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-segment-slider@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/12b80997846fcd4132fd13914ee545578508970c87e6f2d35f604f8de021b1d26051b6ef05e2618bac4d90d82f88b60ab6862bce2aa176b7fea810066004816f + react-native-nitro-modules: 0.35.2 + checksum: 10/bcb4a0ebf1a894252e17ea346505f7d9faf02b9fa678bb166cfcdae915b92e46d5e4437ef1b343516fe261f27de3f2ea696e3dac066b93aa980580016fe76a08 + languageName: node + linkType: hard + +"@onekeyfe/react-native-skeleton@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-skeleton@npm:3.0.82" + peerDependencies: + react: "*" + react-native: "*" + react-native-nitro-modules: 0.35.2 + checksum: 10/28dd987133e409644db3adde37fd5b128abd0d9959b61532f505b22978b153ac8f60bddd5ff8f36120fa597e480b957f4f803d04722ca9130e622e07e8c1b025 languageName: node linkType: hard @@ -9790,14 +9812,14 @@ __metadata: languageName: node linkType: hard -"@onekeyfe/react-native-splash-screen@npm:3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-splash-screen@npm:3.0.78" +"@onekeyfe/react-native-splash-screen@npm:3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-splash-screen@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/df14ced4fe70389ce7e855706eaad6adef7caf9095ca75dfa851456ca2b1d34f9fe47857d65ead0a8656527a342b79b75bead3f3f24875dee4f124b6fdc08bc9 + react-native-nitro-modules: 0.35.2 + checksum: 10/fce0e84447689922f4965f52c47cd8891b0bc863bd6557f78cc3bc53355f42b968a69c4ecff3c0d85d5d0eba22b7e9b3c7d63cfc1c109a3c729843a2d6f075e0 languageName: node linkType: hard @@ -10047,7 +10069,7 @@ __metadata: react-native-confirmation-code-field: "npm:^7.4.0" react-native-copy-asset: "npm:^3.0.2" react-native-draggable-flatlist: "npm:4.0.3" - react-native-get-random-values: "npm:@onekeyfe/react-native-get-random-values@3.0.78" + react-native-get-random-values: "npm:@onekeyfe/react-native-get-random-values@3.0.82" react-native-harness: "npm:1.0.0-alpha.25" react-native-reanimated: "npm:4.2.1" react-native-screens: "npm:4.23.0" @@ -10273,35 +10295,36 @@ __metadata: "@formatjs/intl-pluralrules": "npm:^4.3.3" "@gorhom/bottom-sheet": "npm:5.2.14" "@notifee/react-native": "npm:9.1.8" - "@onekeyfe/react-native-app-update": "npm:3.0.78" - "@onekeyfe/react-native-auto-size-input": "npm:3.0.78" + "@onekeyfe/react-native-app-update": "npm:3.0.82" + "@onekeyfe/react-native-auto-size-input": "npm:3.0.82" "@onekeyfe/react-native-background-thread": "npm:3.0.78" "@onekeyfe/react-native-ble-utils": "npm:0.1.6" - "@onekeyfe/react-native-bundle-crypto": "npm:3.0.78" - "@onekeyfe/react-native-bundle-update": "npm:3.0.78" - "@onekeyfe/react-native-chart-webview": "npm:3.0.78" - "@onekeyfe/react-native-check-biometric-auth-changed": "npm:3.0.78" - "@onekeyfe/react-native-cloud-kit-module": "npm:3.0.78" - "@onekeyfe/react-native-device-utils": "npm:3.0.78" - "@onekeyfe/react-native-keychain-module": "npm:3.0.78" + "@onekeyfe/react-native-bundle-crypto": "npm:3.0.82" + "@onekeyfe/react-native-bundle-update": "npm:3.0.82" + "@onekeyfe/react-native-chart-webview": "npm:3.0.82" + "@onekeyfe/react-native-check-biometric-auth-changed": "npm:3.0.82" + "@onekeyfe/react-native-cloud-kit-module": "npm:3.0.82" + "@onekeyfe/react-native-device-utils": "npm:3.0.82" + "@onekeyfe/react-native-keychain-module": "npm:3.0.82" "@onekeyfe/react-native-lite-card": "npm:3.0.78" - "@onekeyfe/react-native-native-logger": "npm:3.0.78" + "@onekeyfe/react-native-native-logger": "npm:3.0.82" "@onekeyfe/react-native-network-throttle": "npm:3.0.78" - "@onekeyfe/react-native-perf-memory": "npm:3.0.78" - "@onekeyfe/react-native-perf-stats": "npm:3.0.78" - "@onekeyfe/react-native-perp-depth-bar": "npm:3.0.78" - "@onekeyfe/react-native-range-downloader": "npm:3.0.78" - "@onekeyfe/react-native-scroll-guard": "npm:3.0.78" - "@onekeyfe/react-native-segment-slider": "npm:3.0.78" - "@onekeyfe/react-native-skeleton": "npm:3.0.78" + "@onekeyfe/react-native-perf-memory": "npm:3.0.82" + "@onekeyfe/react-native-perf-stats": "npm:3.0.82" + "@onekeyfe/react-native-perp-depth-bar": "npm:3.0.82" + "@onekeyfe/react-native-range-downloader": "npm:3.0.82" + "@onekeyfe/react-native-scroll-guard": "npm:3.0.82" + "@onekeyfe/react-native-segment-slider": "npm:3.0.82" + "@onekeyfe/react-native-skeleton": "npm:3.0.82" "@onekeyfe/react-native-sni-connect": "npm:3.0.78" - "@onekeyfe/react-native-splash-screen": "npm:3.0.78" + "@onekeyfe/react-native-splash-screen": "npm:3.0.82" "@onekeyfe/react-native-split-bundle-loader": "npm:3.0.78" "@onekeyfe/react-native-tab-view": "npm:3.0.78" "@onekeyfe/react-native-text-input": "npm:0.3.0" "@onekeyhq/components": "npm:*" "@onekeyhq/kit": "npm:*" "@onekeyhq/shared": "npm:*" + "@onramper/onramper-react-native": "npm:1.1.1" "@phantom/react-native-juicebox-sdk": "npm:0.3.17" "@react-native-async-storage/async-storage": "npm:@onekeyfe/react-native-async-storage@3.0.78" "@react-native-community/datetimepicker": "npm:8.4.4" @@ -10376,16 +10399,16 @@ __metadata: react-native-fast-pbkdf2: "npm:@onekeyfe/react-native-pbkdf2@3.0.78" react-native-fs: "npm:@dr.pogodin/react-native-fs@2.34.0" react-native-gesture-handler: "npm:2.30.0" - react-native-get-random-values: "npm:@onekeyfe/react-native-get-random-values@3.0.78" + react-native-get-random-values: "npm:@onekeyfe/react-native-get-random-values@3.0.82" react-native-image-colors: "npm:^2.5.0" react-native-image-crop-picker: "npm:0.51.1" react-native-keyboard-controller: "npm:1.20.7" react-native-level-fs: "npm:3.0.1" - react-native-mmkv: "npm:4.1.0" + react-native-mmkv: "npm:4.2.0" react-native-modal: "npm:^13.0.1" react-native-network-info: "npm:@onekeyfe/react-native-network-info@3.0.78" react-native-network-logger: "npm:2.0.1" - react-native-nitro-modules: "npm:0.33.2" + react-native-nitro-modules: "npm:0.35.2" react-native-pager-view: "npm:@onekeyfe/react-native-pager-view@3.0.78" react-native-passkeys: "npm:0.3.3" react-native-permissions: "npm:5.4.4" @@ -10405,7 +10428,7 @@ __metadata: react-native-webview: "npm:13.15.0" react-native-webview-cleaner: "npm:@onekeyfe/react-native-webview-cleaner@1.0.0" react-native-worklets: "npm:0.7.1" - react-native-zip-archive: "npm:@onekeyfe/react-native-zip-archive@3.0.78" + react-native-zip-archive: "npm:@onekeyfe/react-native-zip-archive@3.0.82" readable-stream: "npm:^3.6.0" realm: "npm:20.2.0" realm-flipper-plugin-device: "npm:^1.1.0" @@ -10490,6 +10513,17 @@ __metadata: languageName: unknown linkType: soft +"@onramper/onramper-react-native@npm:1.1.1": + version: 1.1.1 + resolution: "@onramper/onramper-react-native@npm:1.1.1" + peerDependencies: + react: ">=19.0.0" + react-native: ">=0.79.0" + react-native-nitro-modules: ">=0.35.0" + checksum: 10/a5070b193cb7c8fe96d50d9de42a5b98eaff574cc1706c9c44f0a9dd89c94992b90d73adf21c7404e39fd9fae09c89f863c6597f85ef8c41822d9c82f7fa4608 + languageName: node + linkType: hard + "@open-wc/webpack-import-meta-loader@npm:^0.4.7": version: 0.4.7 resolution: "@open-wc/webpack-import-meta-loader@npm:0.4.7" @@ -42296,14 +42330,14 @@ __metadata: languageName: node linkType: hard -"react-native-get-random-values@npm:@onekeyfe/react-native-get-random-values@3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-get-random-values@npm:3.0.78" +"react-native-get-random-values@npm:@onekeyfe/react-native-get-random-values@3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-get-random-values@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/f3266ee56ec0aeb1ac91de033655276dee9b2099311f7554fe4d25c4777fe7c16741ad4213482b9ef9cda624233ef4c8bb932e2c3ef86f4e0bedca4c3d57faba + react-native-nitro-modules: 0.35.2 + checksum: 10/1b578b9a9650aa7a31e92daf40a201af32aaa6647d21d70e697207e847704e0c44ca1cdf706c248a399148070a67b63401800131c53df5cb2be48d47e034cb75 languageName: node linkType: hard @@ -42418,14 +42452,14 @@ __metadata: languageName: node linkType: hard -"react-native-mmkv@npm:4.1.0": - version: 4.1.0 - resolution: "react-native-mmkv@npm:4.1.0" +"react-native-mmkv@npm:4.2.0": + version: 4.2.0 + resolution: "react-native-mmkv@npm:4.2.0" peerDependencies: react: "*" react-native: "*" react-native-nitro-modules: "*" - checksum: 10/306b2b1371bb58152ec562326d93f25e5cf173f31a313ce78202d3681c3cc6f5a0ca792726f94d3482d3bd0fa341735b046223bbe4b85baacba006307d76a792 + checksum: 10/8f0e919eb3add26d47a9d5e171672928c322c89ef92108375ffcc2829d40a5d1727f6b9bed771d1fc7abf76f80695e522d47699c8a891c36a98b04afd3a8d1d1 languageName: node linkType: hard @@ -42474,13 +42508,13 @@ __metadata: languageName: node linkType: hard -"react-native-nitro-modules@npm:0.33.2": - version: 0.33.2 - resolution: "react-native-nitro-modules@npm:0.33.2" +"react-native-nitro-modules@npm:0.35.2": + version: 0.35.2 + resolution: "react-native-nitro-modules@npm:0.35.2" peerDependencies: react: "*" react-native: "*" - checksum: 10/fece339c9f69b256d4e18e6fbf29bc52a16e2e8ea7e70551d8265dbaf455402a67fd8ca9f4e8abe5b6e6d3e33bf6c598d4e980dbbed00a3b791f6fa49c98be17 + checksum: 10/22977e5e22c63fb91526c90fa59fcfe5c7f97978c583400f433a1d3ef8ee32fbceb0ad1b009b5bf0d1794e94a11716ad752fd7702c2d67fc808a0256c6e35488 languageName: node linkType: hard @@ -42895,14 +42929,14 @@ __metadata: languageName: node linkType: hard -"react-native-zip-archive@npm:@onekeyfe/react-native-zip-archive@3.0.78": - version: 3.0.78 - resolution: "@onekeyfe/react-native-zip-archive@npm:3.0.78" +"react-native-zip-archive@npm:@onekeyfe/react-native-zip-archive@3.0.82": + version: 3.0.82 + resolution: "@onekeyfe/react-native-zip-archive@npm:3.0.82" peerDependencies: react: "*" react-native: "*" - react-native-nitro-modules: 0.33.2 - checksum: 10/df04278176755812a86d40016509f9e5f785edb21ebf41db8553c79e6bb47406a27f4cc487f20f88723a1a281e28a02cb02211d19fe002bc90467f2aee6cfdcf + react-native-nitro-modules: 0.35.2 + checksum: 10/d3ee0a9f57bcb3961529d3542aefa75b6dfc5c7359e19f0e04608db01db9666a1fac7476f8dfb9f08710e01ae280f616428a8cffbd2b3e2693f9ee332627992b languageName: node linkType: hard