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