diff --git a/.changeset/gentle-cases-fold.md b/.changeset/gentle-cases-fold.md new file mode 100644 index 000000000..d35b2f747 --- /dev/null +++ b/.changeset/gentle-cases-fold.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ add wallet provisioning diff --git a/.changeset/rich-months-double.md b/.changeset/rich-months-double.md new file mode 100644 index 000000000..9ede42ebe --- /dev/null +++ b/.changeset/rich-months-double.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +🍱 add meawallet config assets diff --git a/.maestro/subflows/activateCard.yaml b/.maestro/subflows/activateCard.yaml index cbba3e087..217a67814 100644 --- a/.maestro/subflows/activateCard.yaml +++ b/.maestro/subflows/activateCard.yaml @@ -8,7 +8,10 @@ appId: ${APP_ID ?? "app.exactly"} commands: - runFlow: { file: scrollTo.yaml, env: { element: Accept and enable card } } - tapOn: Accept and enable card -- assertVisible: Manually add your card to Apple Pay & Google Pay to make contactless payments. +- runFlow: + when: { platform: web } + commands: + - assertVisible: Manually add your card to Apple Pay & Google Pay to make contactless payments. - tapOn: Close - tapOn: Freeze card - tapOn: { text: Freeze card, below: "Freeze your card?" } diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..9bd3c48ea --- /dev/null +++ b/.npmrc @@ -0,0 +1,15 @@ +# isc license +# +# copyright meawallet +# +# permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby +# granted, provided that the above copyright notice and this permission notice appear in all copies. +# +# the software is provided "as is" and the author disclaims all warranties with regard to this software including +# all implied warranties of merchantability and fitness. in no event shall the author be liable for any special, +# direct, indirect, or consequential damages or any damages whatsoever resulting from loss of use, data or profits, +# whether in an action of contract, negligence or other tortious action, arising out of or in connection with the +# use or performance of this software. +@meawallet:registry=https://nexus.ext.meawallet.com/repository/react-native-mpp/ +//nexus.ext.meawallet.com/repository/react-native-mpp/:username=ext-react-native-mpp +//nexus.ext.meawallet.com/repository/react-native-mpp/:_password=OXJDTVo1ZEg2dHVD diff --git a/app.config.ts b/app.config.ts index 1dbe60c11..7c82b3595 100644 --- a/app.config.ts +++ b/app.config.ts @@ -1,7 +1,17 @@ import type { PluginConfigType as BuildPropertiesConfig } from "expo-build-properties/build/pluginConfig"; import type { FontProps } from "expo-font/plugin/build/withFonts"; -import { AndroidConfig, withAndroidManifest, withAppBuildGradle, type ConfigPlugin } from "expo/config-plugins"; +import { + AndroidConfig, + IOSConfig, + withAndroidManifest, + withAppBuildGradle, + withDangerousMod, + withXcodeProject, + type ConfigPlugin, +} from "expo/config-plugins"; +import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; import { env } from "node:process"; import metadata from "./package.json"; @@ -40,6 +50,7 @@ export default { associatedDomains: [`webcredentials:${env.APP_DOMAIN ?? "sandbox.exactly.app"}`], supportsTablet: false, buildNumber: String(versionCode), + entitlements: { "com.apple.developer.payment-pass-provisioning": true }, infoPlist: { ITSAppUsesNonExemptEncryption: false, CFBundleAllowMixedLocalizations: true, @@ -110,12 +121,126 @@ export default { }, ], // @ts-expect-error inline plugin + ((config) => { + const withAndroid = withDangerousMod(config, [ + "android", + (c) => { + const source = path.join(c.modRequest.projectRoot, "src/assets/mea_config"); + const destination = path.join(c.modRequest.projectRoot, "android/app/src/main/res/raw/mea_config"); + if (!existsSync(source)) throw new Error("meawallet: missing src/assets/mea_config"); + mkdirSync(path.dirname(destination), { recursive: true }); + copyFileSync(source, destination); + return c; + }, + ]); + return withXcodeProject(withAndroid, (c) => { + const source = path.join(c.modRequest.projectRoot, "src/assets/mea_config"); + const projectName = c.modRequest.projectName ?? ""; + const destination = path.join(c.modRequest.projectRoot, "ios", projectName, "mea_config"); + if (!existsSync(source)) throw new Error("meawallet: missing src/assets/mea_config"); + copyFileSync(source, destination); + IOSConfig.XcodeUtils.addResourceFileToGroup({ + filepath: `${projectName}/mea_config`, + groupName: projectName, + project: c.modResults, // eslint-disable-line @typescript-eslint/no-unsafe-assignment -- expo xcode project type + isBuildFile: true, + }); + return c; + }); + }) satisfies ConfigPlugin, + // @ts-expect-error inline plugin + ((config) => + withDangerousMod(config, [ + "android", + (c) => { + const buildGradle = path.join(c.modRequest.projectRoot, "android/build.gradle"); + const meaRepo = ` maven { + url "https://nexus.ext.meawallet.com/repository/mpp-android-group/" + credentials { + username = "ext-mpp-android" + password = "M1yeJMcuE5TiGW" + } + }`; + const contents = readFileSync(buildGradle, "utf8"); + if (!contents.includes("nexus.ext.meawallet.com")) { + const replaced = contents.replace(/(allprojects[\s\S]*?repositories\s*\{)/, `$1\n${meaRepo}`); // cspell:ignore allprojects + if (replaced === contents) + throw new Error("meawallet: failed to inject maven repo into android/build.gradle"); + writeFileSync(buildGradle, replaced); + } + return c; + }, + ])) satisfies ConfigPlugin, + // @ts-expect-error inline plugin + ((config) => + withDangerousMod(config, [ + "ios", + (c) => { + const podfile = path.join(c.modRequest.projectRoot, "ios/Podfile"); // cspell:ignore podfile Podfile OBJC RCTJS RCTUI modulemap fmodule + if (!existsSync(podfile)) return c; + const workaround = ` rctHeaders = "#{installer.sandbox.root}/Headers/Public/React-RCTAppDelegate" + Dir.mkdir(rctHeaders) unless Dir.exist?(rctHeaders) + File.write("#{rctHeaders}/React-RCTAppDelegate-umbrella.h", <<~'H') + #ifdef __OBJC__ + #import + #endif + #import "RCTAppDelegate.h" + #import "RCTAppSetupUtils.h" + #import "RCTArchConfiguratorProtocol.h" + #import "RCTDefaultReactNativeFactoryDelegate.h" + #import "RCTDependencyProvider.h" + #import "RCTJSRuntimeConfiguratorProtocol.h" + #import "RCTReactNativeFactory.h" + #import "RCTRootViewFactory.h" + #import "RCTUIConfiguratorProtocol.h" + H + File.write("#{rctHeaders}/React_RCTAppDelegate.modulemap", <<~MAP) + module React_RCTAppDelegate { + umbrella header "React-RCTAppDelegate-umbrella.h" + export * + module * { export * } + } + MAP + installer.pods_project.targets.each do |target| + next unless target.name == "meawallet-react-native-mpp" + target.build_configurations.each do |buildConfiguration| + flags = buildConfiguration.build_settings["OTHER_SWIFT_FLAGS"] || "$(inherited)" + next if flags.include?("React_RCTAppDelegate.modulemap") + buildConfiguration.build_settings["OTHER_SWIFT_FLAGS"] = + "#{flags} -Xcc -fmodule-map-file=\${PODS_ROOT}/Headers/Public/React-RCTAppDelegate/React_RCTAppDelegate.modulemap" + end + end +`; + const contents = readFileSync(podfile, "utf8"); + if (!contents.includes("React_RCTAppDelegate.modulemap")) { + const replaced = contents.replace( + /(\s{4}react_native_post_install\([\s\S]*?\n\s{4}\)\n)/, + `$1${workaround}`, + ); + if (replaced === contents) + throw new Error("meawallet: failed to inject react_native_post_install workaround into ios/Podfile"); + writeFileSync(podfile, replaced); + } + return c; + }, + ])) satisfies ConfigPlugin, + // @ts-expect-error inline plugin ((config) => withAndroidManifest( withAppBuildGradle(config, (c) => { - c.modResults.contents = c.modResults.contents.replace( - /defaultConfig\s*\{/, - '$& ndk { debugSymbolLevel "FULL" }', + c.modResults.contents = c.modResults.contents.replaceAll( + /(defaultConfig\s*\{)(?:\s*ndk\s*\{\s*debugSymbolLevel\s*"FULL"\s*\})+/g, + "$1", + ); + if (!c.modResults.contents.includes('debugSymbolLevel "FULL"')) { + c.modResults.contents = c.modResults.contents.replace( + /release\s*\{/, + '$&\n ndk { debugSymbolLevel "FULL" }', + ); + } + c.modResults.contents = c.modResults.contents.replaceAll( + '\nimplementation(enforcedPlatform("com.squareup.okhttp3:okhttp-bom:4.12.0"))', + "", ); c.modResults.contents = c.modResults.contents.replace( /dependencies\s*\{/, diff --git a/cspell.json b/cspell.json index 7e60030e9..d1b60554f 100644 --- a/cspell.json +++ b/cspell.json @@ -102,6 +102,7 @@ "mainqueg", "mateo-soso", "mdpi", + "meawallet", "memester", "miniapp", "mipd", diff --git a/eas.json b/eas.json index 1f438ca06..d8d4420cb 100644 --- a/eas.json +++ b/eas.json @@ -5,6 +5,7 @@ "resourceClass": "large", "node": "24.14.1", "pnpm": "10.33.0", + "env": { "npm_config_mpp_env": "prod" }, "android": { "image": "latest" }, "ios": { "image": "latest" } }, diff --git a/package.json b/package.json index a59606606..c4779e4ae 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@intercom/intercom-react-native": "^9.8.0", "@intercom/messenger-js-sdk": "^0.0.18", "@lifi/sdk": "3.7.7", + "@meawallet/react-native-mpp": "^2.2.2", "@peculiar/asn1-ecc": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/webcrypto": "^1.5.0", @@ -167,6 +168,9 @@ "packageManager": "pnpm@10.33.0", "pnpm": { "overrides": { + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", + "eslint-plugin-n": "17.24.0", "@wagmi/core": "catalog:", "abitype>zod": "^4.0.0", "comlink": "$comlink", @@ -204,6 +208,7 @@ }, "patchedDependencies": { "@lifi/sdk": "patches/@lifi__sdk.patch", + "@meawallet/react-native-mpp": "patches/@meawallet__react-native-mpp.patch", "embedded-postgres": "patches/embedded-postgres.patch", "eslint-config-universe": "patches/eslint-config-universe.patch" }, diff --git a/patches/@meawallet__react-native-mpp.patch b/patches/@meawallet__react-native-mpp.patch new file mode 100644 index 000000000..06b9d61fb --- /dev/null +++ b/patches/@meawallet__react-native-mpp.patch @@ -0,0 +1,223 @@ +diff --git a/ios/ApplePay.mm b/ios/ApplePay.mm +index d5cb54b..394130d 100644 +--- a/ios/ApplePay.mm ++++ b/ios/ApplePay.mm +@@ -36,8 +36,34 @@ @implementation ApplePay + GET_TURBO_MODULE(ApplePay) + + static NSString *APPLE_PAY_EVENT = @"ApplePayDataChanged"; ++static NSString *APPLE_PAY_TRACE_EVENT = @"ApplePayTrace"; + static NSSet *PAYMENT_NETWORKS; + ++static NSString *redactedTail(NSString *value, NSUInteger count) ++{ ++ if (!value) return nil; ++ if (value.length <= count) return [@"…" stringByAppendingString:value]; ++ return [@"…" stringByAppendingString:[value substringFromIndex:value.length - count]]; ++} ++ ++static NSDictionary *traceError(NSError *error) ++{ ++ if (!error) return @{}; ++ ++ NSMutableDictionary *details = [NSMutableDictionary new]; ++ if (error.domain) details[@"domain"] = error.domain; ++ details[@"code"] = @(error.code); ++ if (error.localizedDescription) details[@"description"] = error.localizedDescription; ++ if (error.localizedFailureReason) details[@"failureReason"] = error.localizedFailureReason; ++ if (error.localizedRecoverySuggestion) details[@"recoverySuggestion"] = error.localizedRecoverySuggestion; ++ NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey]; ++ if (underlyingError) details[@"underlyingError"] = traceError(underlyingError); ++ if (error.userInfo.count > 0) { ++ details[@"userInfoKeys"] = error.userInfo.allKeys; ++ details[@"userInfo"] = [NSString stringWithFormat:@"%@", error.userInfo]; ++ } ++ return details; ++} + RCT_EXPORT_MODULE() + + - (instancetype)init +@@ -106,7 +125,21 @@ - (instancetype)init + + - (NSArray *)supportedEvents + { +- return @[APPLE_PAY_EVENT]; ++ return @[APPLE_PAY_EVENT, APPLE_PAY_TRACE_EVENT]; ++} ++ ++- (void)emitTrace:(NSString *)event details:(NSDictionary *)details ++{ ++ NSMutableDictionary *body = [NSMutableDictionary new]; ++ body[@"event"] = event; ++ body[@"timestamp"] = @((long long)([[NSDate date] timeIntervalSince1970] * 1000)); ++ if (details.count > 0) [body addEntriesFromDictionary:details]; ++ NSLog(@"ApplePayTrace %@", body); ++ if (self.hasListeners) { ++ dispatch_async(dispatch_get_main_queue(), ^{ ++ [self sendEventWithName:APPLE_PAY_TRACE_EVENT body:body]; ++ }); ++ } + } + + - (void)startObserving +@@ -127,6 +160,7 @@ + (BOOL)requiresMainQueueSetup + RCT_EXPORT_METHOD(setDebugLoggingEnabled:(BOOL)enabled) + { + RCTLogInfo(@"setDebugLoggingEnabled: %i", enabled); ++ [self emitTrace:@"setDebugLoggingEnabled" details:@{ @"enabled": @(enabled) }]; + + RCTSetLogThreshold(enabled ? RCTLogLevelTrace : RCTLogLevelFatal); + +@@ -163,7 +197,11 @@ + (BOOL)requiresMainQueueSetup + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) + { +- RCTLogInfo(@"initializeOemTokenization: %@", card); ++ NSMutableDictionary *requestDetails = [NSMutableDictionary new]; ++ requestDetails[@"cardKeys"] = card.allKeys ?: @[]; ++ requestDetails[@"hasCardSecret"] = @(card[@"cardSecret"] != nil); ++ if (card[@"cardDataType"]) requestDetails[@"cardDataType"] = [NSString stringWithFormat:@"%@", card[@"cardDataType"]]; ++ [self emitTrace:@"initializeOemTokenization.request" details:requestDetails]; + + [MeaPushProvisioning initializeOemTokenization:[Serialization reactToCardDataParameters:card] + completionHandler:^(MppInitializeOemTokenizationResponseData *data, NSError *error) { +@@ -171,6 +209,17 @@ + (BOOL)requiresMainQueueSetup + + if (data && [data isValid]) { + RCTLogInfo(@"initializeOemTokenization:completionHandler: data isValid"); ++ NSMutableDictionary *details = [NSMutableDictionary new]; ++ details[@"hasPrimaryAccountIdentifier"] = @(data.primaryAccountIdentifier.length > 0); ++ if (data.localizedDescription) details[@"localizedDescription"] = data.localizedDescription; ++ if (data.networkName) details[@"networkName"] = data.networkName; ++ NSString *identifier = redactedTail(data.primaryAccountIdentifier, 8); ++ if (identifier) details[@"primaryAccountIdentifier"] = identifier; ++ if (data.primaryAccountIdentifier) details[@"primaryAccountIdentifierLength"] = @(data.primaryAccountIdentifier.length); ++ if (data.primaryAccountSuffix) details[@"primaryAccountSuffix"] = data.primaryAccountSuffix; ++ if (data.tokenizationReceipt) details[@"tokenizationReceiptLength"] = @(data.tokenizationReceipt.length); ++ details[@"validFor"] = @(data.validFor); ++ [self emitTrace:@"initializeOemTokenization.success" details:details]; + + resolve([Serialization initializeOemTokenizationResponseDataToReact:data]); + } +@@ -181,6 +228,11 @@ + (BOOL)requiresMainQueueSetup + userInfo:@{NSLocalizedDescriptionKey:@"Invalid Tokenization Response Data." }]; + } + ++ NSMutableDictionary *details = [NSMutableDictionary new]; ++ details[@"hasData"] = @(data != nil); ++ details[@"isValid"] = @([data isValid]); ++ [details addEntriesFromDictionary:traceError(error)]; ++ [self emitTrace:@"initializeOemTokenization.error" details:details]; + RCTLogError(@"initializeOemTokenization:completionHandler: error: %@", error); + + reject(@(error.code).stringValue, [NSString stringWithFormat:@"%@", error], error); +@@ -299,7 +351,16 @@ + (BOOL)requiresMainQueueSetup + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) + { +- RCTLogInfo(@"showAddPaymentPassView: %@", tokenizationData); ++ NSMutableDictionary *requestDetails = [NSMutableDictionary new]; ++ requestDetails[@"hasPrimaryAccountIdentifier"] = @(tokenizationData[@"primaryAccountIdentifier"] != nil); ++ requestDetails[@"keys"] = tokenizationData.allKeys ?: @[]; ++ if (tokenizationData[@"localizedDescription"]) requestDetails[@"localizedDescription"] = tokenizationData[@"localizedDescription"]; ++ if (tokenizationData[@"networkName"]) requestDetails[@"networkName"] = tokenizationData[@"networkName"]; ++ if (tokenizationData[@"primaryAccountIdentifier"]) requestDetails[@"primaryAccountIdentifier"] = redactedTail([NSString stringWithFormat:@"%@", tokenizationData[@"primaryAccountIdentifier"]], 8); ++ if (tokenizationData[@"primaryAccountSuffix"]) requestDetails[@"primaryAccountSuffix"] = tokenizationData[@"primaryAccountSuffix"]; ++ if (tokenizationData[@"tokenizationReceipt"]) requestDetails[@"tokenizationReceiptLength"] = @([[NSString stringWithFormat:@"%@", tokenizationData[@"tokenizationReceipt"]] length]); ++ if (tokenizationData[@"validFor"]) requestDetails[@"validFor"] = tokenizationData[@"validFor"]; ++ [self emitTrace:@"showAddPaymentPassView.request" details:requestDetails]; + + @synchronized (self) { + /* +@@ -324,6 +384,7 @@ + (BOOL)requiresMainQueueSetup + if (self.paymentPassController) { + + [RCTPresentedViewController() presentViewController:self.paymentPassController animated:YES completion:^{ ++ [self emitTrace:@"showAddPaymentPassView.presented" details:@{}]; + RCTLogInfo(@"showAddPaymentPassView: paymentPassController presented"); + }]; + } else { +@@ -331,6 +392,7 @@ + (BOOL)requiresMainQueueSetup + code:MPP_ERROR_API + userInfo:@{NSLocalizedDescriptionKey:@"PKAddPaymentPassViewController can't be created."}]; + ++ [self emitTrace:@"showAddPaymentPassView.error" details:traceError(error)]; + RCTLogError(@"showAddPaymentPassView: error: %@", error); + + self.tokenizationResponseData = nil; +@@ -343,6 +405,7 @@ + (BOOL)requiresMainQueueSetup + code:MPP_ERROR_API + userInfo:@{NSLocalizedDescriptionKey:@"showAddPaymentPassView() already running."}]; + ++ [self emitTrace:@"showAddPaymentPassView.error" details:traceError(error)]; + RCTLogError(@"showAddPaymentPassView: error: %@", error); + + reject(@(error.code).stringValue, [NSString stringWithFormat:@"%@", error], error); +@@ -657,6 +720,12 @@ - (void)addPaymentPassViewController:(PKAddPaymentPassViewController *)controlle + nonceSignature:(NSData *)nonceSignature + completionHandler:(void(^)(PKAddPaymentPassRequest *request))handler + { ++ NSMutableDictionary *details = [NSMutableDictionary new]; ++ details[@"certificateCount"] = @(certificates.count); ++ details[@"nonceLength"] = @(nonce.length); ++ details[@"nonceSignatureLength"] = @(nonceSignature.length); ++ if (self.tokenizationResponseData.tokenizationReceipt) details[@"tokenizationReceiptLength"] = @(self.tokenizationResponseData.tokenizationReceipt.length); ++ [self emitTrace:@"generateRequestWithCertificateChain" details:details]; + RCTLogInfo(@"addPaymentPassViewController:generateRequestWithCertificateChain:"); + + @synchronized (self) { +@@ -670,6 +739,13 @@ - (void)addPaymentPassViewController:(PKAddPaymentPassViewController *)controlle + if (valid && !error) { + PKAddPaymentPassRequest *addPaymentPassRequest = [data addPaymentPassRequest]; + ++ NSMutableDictionary *details = [NSMutableDictionary new]; ++ details[@"hasRequest"] = @(addPaymentPassRequest != nil); ++ details[@"isValid"] = @(valid); ++ if (addPaymentPassRequest.activationData) details[@"activationDataLength"] = @(addPaymentPassRequest.activationData.length); ++ if (addPaymentPassRequest.encryptedPassData) details[@"encryptedPassDataLength"] = @(addPaymentPassRequest.encryptedPassData.length); ++ if (addPaymentPassRequest.ephemeralPublicKey) details[@"ephemeralPublicKeyLength"] = @(addPaymentPassRequest.ephemeralPublicKey.length); ++ [self emitTrace:@"completeOemTokenization.success" details:details]; + RCTLogInfo(@"addPaymentPassViewController:generateRequestWithCertificateChain: addPaymentPassRequest created"); + + self.tokenizationResponseData = nil; +@@ -682,6 +755,11 @@ - (void)addPaymentPassViewController:(PKAddPaymentPassViewController *)controlle + userInfo:@{ NSLocalizedDescriptionKey:@"Can't add payment pass." }]; + } + ++ NSMutableDictionary *details = [NSMutableDictionary new]; ++ details[@"hasData"] = @(data != nil); ++ details[@"isValid"] = @(valid); ++ [details addEntriesFromDictionary:traceError(error)]; ++ [self emitTrace:@"completeOemTokenization.error" details:details]; + RCTLogError(@"addPaymentPassViewController:generateRequestWithCertificateChain: error: %@", error); + } + }]; +@@ -698,11 +776,17 @@ - (void)addPaymentPassViewController:(PKAddPaymentPassViewController *)controlle + self.tokenizationResponseData = nil; + + if (!error) { ++ NSString *activationState = [Serialization getActivateStateString:pass.activationState]; ++ [self emitTrace:@"didFinishAddingPaymentPass.success" details:@{ @"activationState": activationState ?: @"unknown" }]; + RCTLogInfo(@"addPaymentPassViewController:didFinishAddingPaymentPass: pass.activationState: %lu", (unsigned long)pass.activationState); + +- self.resolve([Serialization getActivateStateString:pass.activationState]); ++ self.resolve(activationState); + } + else { ++ NSMutableDictionary *details = [traceError(error) mutableCopy]; ++ details[@"hasPass"] = @(pass != nil); ++ if (pass) details[@"activationState"] = [Serialization getActivateStateString:pass.activationState] ?: @"unknown"; ++ [self emitTrace:@"didFinishAddingPaymentPass.error" details:details]; + RCTLogError(@"addPaymentPassViewController:didFinishAddingPaymentPass: error: %@", error); + + self.reject(@(error.code).stringValue, [NSString stringWithFormat:@"%@", error], error); +diff --git a/meawallet-react-native-mpp.podspec b/meawallet-react-native-mpp.podspec +index 05795f1..7396ff2 100644 +--- a/meawallet-react-native-mpp.podspec ++++ b/meawallet-react-native-mpp.podspec +@@ -21,4 +21,5 @@ Pod::Spec.new do |s| + s.xcconfig = { "GCC_PREPROCESSOR_DEFINITIONS" => "REACT_NATIVE_MPP_VERSION=\"@\\\"#{s.version}\\\"\"" } + + install_modules_dependencies(s) ++ s.dependency "React-RCTAppDelegate" + end diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c462ad3da..1d8737e93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ catalogs: version: 3.6.0 overrides: + '@typescript-eslint/eslint-plugin': 8.58.0 + '@typescript-eslint/parser': 8.58.0 + eslint-plugin-n: 17.24.0 '@wagmi/core': ^3.4.1 abitype>zod: ^4.0.0 comlink: ^4.4.2 @@ -104,6 +107,9 @@ patchedDependencies: '@lifi/sdk': hash: ee16233f297d9a6c8a8320b5dc2b4bf47b7be8b481d79e3d54108cd77775b45b path: patches/@lifi__sdk.patch + '@meawallet/react-native-mpp': + hash: 7aef5e11a2777ba755c435c4b08bf7081a655da89f1560daf744f8f2c780029c + path: patches/@meawallet__react-native-mpp.patch embedded-postgres: hash: cb5e37525b1810f2af136570b38d5e0cec4cc2455408896ed1943d27f3f61b38 path: patches/embedded-postgres.patch @@ -154,6 +160,9 @@ importers: '@lifi/sdk': specifier: 3.7.7 version: 3.7.7(patch_hash=ee16233f297d9a6c8a8320b5dc2b4bf47b7be8b481d79e3d54108cd77775b45b)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6) + '@meawallet/react-native-mpp': + specifier: ^2.2.2 + version: 2.3.0(patch_hash=7aef5e11a2777ba755c435c4b08bf7081a655da89f1560daf744f8f2c780029c)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) '@peculiar/asn1-ecc': specifier: ^2.6.1 version: 2.6.1 @@ -412,7 +421,7 @@ importers: version: 22.6.2(@babel/traverse@7.29.0)(@zkochan/js-yaml@0.0.7)(eslint@9.39.4)(nx@22.6.2) '@nx/eslint-plugin': specifier: 22.6.2 - version: 22.6.2(@babel/traverse@7.29.0)(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(nx@22.6.2)(typescript@5.9.3) + version: 22.6.2(@babel/traverse@7.29.0)(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(nx@22.6.2)(typescript@5.9.3) '@nx/js': specifier: 22.6.2 version: 22.6.2(@babel/traverse@7.29.0)(nx@22.6.2) @@ -563,7 +572,7 @@ importers: version: 0.2.22 '@nx/eslint-plugin': specifier: ^22.6.2 - version: 22.6.2(@babel/traverse@7.29.0)(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(nx@22.6.2)(typescript@5.9.3) + version: 22.6.2(@babel/traverse@7.29.0)(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(nx@22.6.2)(typescript@5.9.3) '@tanstack/eslint-plugin-query': specifier: ^5.95.2 version: 5.95.2(eslint@9.39.4)(typescript@5.9.3) @@ -575,7 +584,7 @@ importers: version: 3.0.1 '@vitest/eslint-plugin': specifier: ^1.6.13 - version: 1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)(vitest@4.1.2) + version: 1.6.13(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)(vitest@4.1.2) '@wagmi/cli': specifier: ^2.10.0 version: 2.10.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -593,7 +602,7 @@ importers: version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.4) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4) + version: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4) eslint-plugin-jsdoc: specifier: ^62.8.1 version: 62.8.1(eslint@9.39.4) @@ -601,7 +610,7 @@ importers: specifier: ^6.10.2 version: 6.10.2(eslint@9.39.4) eslint-plugin-n: - specifier: ^17.24.0 + specifier: 17.24.0 version: 17.24.0(eslint@9.39.4)(typescript@5.9.3) eslint-plugin-perfectionist: specifier: ^5.7.0 @@ -3677,6 +3686,13 @@ packages: '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + '@meawallet/react-native-mpp@2.3.0': + resolution: {integrity: sha512-KuWSjSprQReFQUpcfdl83aX3y9irLkoCRGY3X9AzIUYusFat3PZbNstiRvkeSZ5Cpa6LStzAts+SC+niVd9t8g==} + engines: {node: '>=18'} + peerDependencies: + react: '*' + react-native: '*' + '@mermaid-js/parser@1.1.1': resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} @@ -3808,7 +3824,7 @@ packages: '@nx/eslint-plugin@22.6.2': resolution: {integrity: sha512-wrq+MwZ2QErQdm7XiI1jLSsJ658Yg7sR12gZLTyRfKvTFZIqBMQrOBd8v1IaRc+ZLUUiv9hqhzteLm/+EGAYuA==} peerDependencies: - '@typescript-eslint/parser': ^6.13.2 || ^7.0.0 || ^8.0.0 + '@typescript-eslint/parser': 8.58.0 eslint-config-prettier: ^10.0.0 peerDependenciesMeta: eslint-config-prettier: @@ -6207,16 +6223,16 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@typescript-eslint/eslint-plugin@8.57.2': - resolution: {integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==} + '@typescript-eslint/eslint-plugin@8.58.0': + resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.57.2 + '@typescript-eslint/parser': 8.58.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ^5.9.3 - '@typescript-eslint/parser@8.57.2': - resolution: {integrity: sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==} + '@typescript-eslint/parser@8.58.0': + resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -6234,6 +6250,12 @@ packages: peerDependencies: typescript: ^5.9.3 + '@typescript-eslint/project-service@8.58.0': + resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: ^5.9.3 + '@typescript-eslint/scope-manager@8.56.1': resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6242,6 +6264,10 @@ packages: resolution: {integrity: sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.58.0': + resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.56.1': resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6254,6 +6280,12 @@ packages: peerDependencies: typescript: ^5.9.3 + '@typescript-eslint/tsconfig-utils@8.58.0': + resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: ^5.9.3 + '@typescript-eslint/type-utils@8.57.2': resolution: {integrity: sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6261,6 +6293,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ^5.9.3 + '@typescript-eslint/type-utils@8.58.0': + resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ^5.9.3 + '@typescript-eslint/types@8.56.1': resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6269,6 +6308,10 @@ packages: resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.58.0': + resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.56.1': resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6281,6 +6324,12 @@ packages: peerDependencies: typescript: ^5.9.3 + '@typescript-eslint/typescript-estree@8.58.0': + resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: ^5.9.3 + '@typescript-eslint/utils@8.56.1': resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6295,6 +6344,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ^5.9.3 + '@typescript-eslint/utils@8.58.0': + resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ^5.9.3 + '@typescript-eslint/visitor-keys@8.56.1': resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6303,6 +6359,10 @@ packages: resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.58.0': + resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -6430,7 +6490,7 @@ packages: resolution: {integrity: sha512-ui7JGWBoQpS5NKKW0FDb1eTuFEZ5EupEv2Psemuyfba7DfA5K52SeDLelt6P4pQJJ/4UGkker/BgMk/KrjH3WQ==} engines: {node: '>=18'} peerDependencies: - '@typescript-eslint/eslint-plugin': '*' + '@typescript-eslint/eslint-plugin': 8.58.0 eslint: '>=8.57.0' typescript: ^5.9.3 vitest: '*' @@ -17163,6 +17223,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@meawallet/react-native-mpp@2.3.0(patch_hash=7aef5e11a2777ba755c435c4b08bf7081a655da89f1560daf744f8f2c780029c)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)': + dependencies: + react: 19.2.0 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10) + '@mermaid-js/parser@1.1.1': dependencies: '@chevrotain/types': 11.1.2 @@ -17333,12 +17398,12 @@ snapshots: - supports-color - verdaccio - '@nx/eslint-plugin@22.6.2(@babel/traverse@7.29.0)(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(nx@22.6.2)(typescript@5.9.3)': + '@nx/eslint-plugin@22.6.2(@babel/traverse@7.29.0)(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(nx@22.6.2)(typescript@5.9.3)': dependencies: '@nx/devkit': 22.6.2(nx@22.6.2) '@nx/js': 22.6.2(@babel/traverse@7.29.0)(nx@22.6.2) '@phenomnomnominal/tsquery': 6.1.4(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/type-utils': 8.57.2(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/utils': 8.57.2(eslint@9.39.4)(typescript@5.9.3) chalk: 4.1.2 @@ -20658,14 +20723,14 @@ snapshots: '@types/node': 25.5.0 optional: true - '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.2 + '@typescript-eslint/parser': 8.58.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 eslint: 9.39.4 ignore: 7.0.5 natural-compare: 1.4.0 @@ -20674,12 +20739,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.2 + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 debug: 4.4.3 eslint: 9.39.4 typescript: 5.9.3 @@ -20688,8 +20753,8 @@ snapshots: '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -20704,6 +20769,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.58.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.56.1': dependencies: '@typescript-eslint/types': 8.56.1 @@ -20714,6 +20788,11 @@ snapshots: '@typescript-eslint/types': 8.57.2 '@typescript-eslint/visitor-keys': 8.57.2 + '@typescript-eslint/scope-manager@8.58.0': + dependencies: + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -20722,6 +20801,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.58.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/type-utils@8.57.2(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.57.2 @@ -20734,10 +20817,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.58.0(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.4)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@8.56.1': {} '@typescript-eslint/types@8.57.2': {} + '@typescript-eslint/types@8.58.0': {} + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) @@ -20768,6 +20865,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.58.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.58.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.56.1(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) @@ -20790,6 +20902,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.58.0(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.56.1': dependencies: '@typescript-eslint/types': 8.56.1 @@ -20800,6 +20923,11 @@ snapshots: '@typescript-eslint/types': 8.57.2 eslint-visitor-keys: 5.0.1 + '@typescript-eslint/visitor-keys@8.58.0': + dependencies: + '@typescript-eslint/types': 8.58.0 + eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.0': {} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -20884,13 +21012,13 @@ snapshots: tinyrainbow: 3.1.0 vitest: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(@vitest/ui@4.1.2)(vite@8.0.5(@types/node@25.5.0)(esbuild@0.27.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)(vitest@4.1.2)': + '@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)(vitest@4.1.2)': dependencies: '@typescript-eslint/scope-manager': 8.57.2 '@typescript-eslint/utils': 8.57.2(eslint@9.39.4)(typescript@5.9.3) eslint: 9.39.4 optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) typescript: 5.9.3 vitest: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(@vitest/ui@4.1.2)(vite@8.0.5(@types/node@25.5.0)(esbuild@0.27.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: @@ -23124,11 +23252,11 @@ snapshots: eslint-config-universe@15.0.3(patch_hash=cfe35ddf48bd80b3df25a30d7d8878394712a39321d2855dcb90a64df2ccc280)(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4)(prettier@3.8.1)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.4)(typescript@5.9.3) eslint: 9.39.4 eslint-config-prettier: 9.1.2(eslint@9.39.4) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4) eslint-plugin-n: 17.24.0(eslint@9.39.4)(typescript@5.9.3) eslint-plugin-node: 11.1.0(eslint@9.39.4) eslint-plugin-prettier: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@9.39.4))(eslint@9.39.4)(prettier@3.8.1) @@ -23170,15 +23298,15 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.4)(typescript@5.9.3) eslint: 9.39.4 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.4) @@ -23216,7 +23344,7 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -23227,7 +23355,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -23239,7 +23367,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.4)(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -29550,8 +29678,8 @@ snapshots: typescript-eslint@8.57.2(eslint@9.39.4)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) '@typescript-eslint/utils': 8.57.2(eslint@9.39.4)(typescript@5.9.3) eslint: 9.39.4 diff --git a/src/assets/images/google-wallet-button.svg b/src/assets/images/google-wallet-button.svg new file mode 100644 index 000000000..67e9346a7 --- /dev/null +++ b/src/assets/images/google-wallet-button.svg @@ -0,0 +1,12 @@ + + + + + + + + + + Add to + Google Wallet + diff --git a/src/assets/images/google-wallet-icon.svg b/src/assets/images/google-wallet-icon.svg new file mode 100644 index 000000000..346bcb22f --- /dev/null +++ b/src/assets/images/google-wallet-icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/mea_config b/src/assets/mea_config new file mode 100644 index 000000000..671a1be2d Binary files /dev/null and b/src/assets/mea_config differ diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx index 11830d537..1a96a5099 100644 --- a/src/components/card/Card.tsx +++ b/src/components/card/Card.tsx @@ -1,6 +1,6 @@ -import React, { useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; -import { RefreshControl } from "react-native"; +import { Alert, NativeEventEmitter, NativeModules, Platform, Pressable, RefreshControl } from "react-native"; import { selectionAsync } from "expo-haptics"; import { useRouter } from "expo-router"; @@ -22,10 +22,12 @@ import CardPIN from "./CardPIN"; import ExaCard from "./exa-card/ExaCard"; import SpendingLimits from "./SpendingLimits"; import VerificationFailure from "./VerificationFailure"; +import GoogleWalletButton from "../../assets/images/google-wallet-button.svg"; +import GoogleWalletIcon from "../../assets/images/google-wallet-icon.svg"; import { presentArticle } from "../../utils/intercom"; import openBrowser from "../../utils/openBrowser"; import queryClient from "../../utils/queryClient"; -import reportError from "../../utils/reportError"; +import reportError, { classifyError } from "../../utils/reportError"; import { APIError, createCard, @@ -50,6 +52,196 @@ import Text from "../shared/Text"; import View from "../shared/View"; import type { Credential } from "@exactly/common/validation"; +import type { GooglePayTokenInfo, TokenInfo } from "@meawallet/react-native-mpp"; + +type GoogleToken = GooglePayTokenInfo | TokenInfo; +type Wallet = { + default: { + ApplePay: { + AddPassButton?: React.ComponentType<{ + addPassButtonStyle: "black"; + onPress: () => void; + style: { height: number; width: number }; + }>; + canAddPaymentPass(): Promise; + canAddPaymentPassWithPrimaryAccountIdentifier(primaryAccountIdentifier: string): Promise; + canAddRemoteSecureElementPassWithPrimaryAccountIdentifier(primaryAccountIdentifier: string): Promise; + canAddSecureElementPassWithPrimaryAccountIdentifier(primaryAccountIdentifier: string): Promise; + initializeOemTokenization(cardData: unknown): Promise<{ + localizedDescription?: string; + networkName?: string; + primaryAccountIdentifier?: string; + primaryAccountSuffix?: string; + tokenizationReceipt?: string; + validFor?: number; + }>; + isPassLibraryAvailable(): Promise; + isWatchPaired(): Promise; + registerDataChangedListener(listener: (data?: unknown) => void): unknown; + remoteSecureElementPassExistsWithPrimaryAccountIdentifier(primaryAccountIdentifier: string): Promise; + removeDataChangedListener(subscription: unknown): void; + secureElementPassExistsWithPrimaryAccountIdentifier(primaryAccountIdentifier: string): Promise; + setDebugLoggingEnabled(enabled: boolean): void; + showAddPaymentPassView(response: unknown): Promise; + }; + GooglePay: { + activateWithTokenInfo(token: TokenInfo): Promise; + checkWalletForCardToken(cardData: unknown): Promise; + isWalletAvailable(): Promise; + push(cardData: unknown, cardDisplayName: string, userAddress: object): Promise; + registerDataChangedListener(listener: () => void): unknown; + removeDataChangedListener(subscription: unknown): void; + tokenize(token: GooglePayTokenInfo, cardDisplayName: string): Promise; + }; + initialize(): Promise; + }; + MppCardDataParameters: { + withCardSecret(cardId: string, cardSecret: string): unknown; + }; +}; +type WalletEligibility = { apple: boolean; google: "added" | "cta" | "hidden"; googleToken: GoogleToken | null }; +const hiddenWallet = { apple: false, google: "hidden", googleToken: null } satisfies WalletEligibility; + +const provisioningSdk = Platform.OS === "web" ? undefined : (require("@meawallet/react-native-mpp") as Wallet); // eslint-disable-line unicorn/prefer-module +let walletInitPromise: Promise | undefined; +const primaryAccountIdentifiers = new Map(); +const redactedWalletKeys = new Set([ + "activationData", + "cardNumber", + "cardSecret", + "certificates", + "cvv", + "encryptedPassData", + "ephemeralPublicKey", + "nonce", + "nonceSignature", + "pan", + "tokenizationReceipt", + "wrappedKey", +]); + +function redactWalletValue(value: unknown, key?: string, seen = new WeakSet()): unknown { + if (value == null || typeof value === "boolean" || typeof value === "number") return value; + if (typeof value === "string") { + if (key !== undefined && redactedWalletKeys.has(key)) return `[redacted:${value.length}]`; + if (key?.toLowerCase().includes("identifier")) return `…${value.slice(-8)}`; + return value.length > 240 ? `${value.slice(0, 237)}...` : value; + } + if (value instanceof Error) { + return redactWalletValue( + { + cause: value.cause, + message: value.message, + name: value.name, + }, + key, + seen, + ); + } + if (Array.isArray(value)) return value.map((item) => redactWalletValue(item, key, seen)); + if (typeof value === "object") { + if (seen.has(value)) return "[circular]"; + seen.add(value); + const result: Record = {}; + for (const [entryKey, entryValue] of Object.entries(value)) { + result[entryKey] = redactWalletValue(entryValue, entryKey, seen); + } + return result; + } + return "[unsupported]"; +} + +function walletLog(event: string, details?: Record) { + console.log(`[wallet] ${JSON.stringify(redactWalletValue(details ? { event, ...details } : { event }))}`); // eslint-disable-line no-console -- temporary provisioning trace +} + +function summarizeWalletError(error: unknown) { + const classification = classifyError(error); + const value = + typeof error === "object" && error !== null + ? (error as { cause?: unknown; code?: unknown; domain?: unknown; message?: unknown; name?: unknown }) + : undefined; + return redactWalletValue({ + cause: value?.cause, + classification: { + known: classification.known, + knownInfo: classification.knownInfo, + knownWarning: classification.knownWarning, + walletCancelled: classification.walletCancelled, + walletRejected: classification.walletRejected, + }, + code: value?.code, + domain: value?.domain, + message: error instanceof Error ? error.message : (value?.message ?? String(error)), + name: error instanceof Error ? error.name : value?.name, + userInfo: "userInfo" in (value ?? {}) ? (value as { userInfo?: unknown }).userInfo : undefined, + }) as Record; +} + +async function traceWalletCall(event: string, work: () => Promise, details?: Record) { + const startedAt = Date.now(); + walletLog(`${event}.start`, details); + try { + const result = await work(); + walletLog(`${event}.success`, { ...details, durationMs: Date.now() - startedAt, result }); + return result; + } catch (error) { + walletLog(`${event}.error`, { ...details, durationMs: Date.now() - startedAt, error: summarizeWalletError(error) }); + throw error; + } +} +function getApplePayEmitter() { + if (Platform.OS !== "ios" || !NativeModules.ApplePay) return; + try { + return new NativeEventEmitter(NativeModules.ApplePay as never); + } catch (error) { + walletLog("apple.nativeEmitter.error", { error: summarizeWalletError(error) }); + } +} + +function enableWalletDebugLogging(wallet: Wallet) { + if (Platform.OS !== "ios") return; + try { + wallet.default.ApplePay.setDebugLoggingEnabled(true); + walletLog("apple.debug.enabled", { enabled: true }); + } catch (error) { + walletLog("apple.debug.enable.error", { error: summarizeWalletError(error) }); + } +} + +function isGoogleToken(token: unknown): token is GoogleToken { + return typeof token === "object" && token !== null && "tokenState" in token && typeof token.tokenState === "string"; +} + +const activeTokenStates = new Set(["TOKEN_STATE_ACTIVE"]); +const pushTokenStates = new Set(["TOKEN_STATE_NOT_FOUND", "TOKEN_STATE_UNTOKENIZED"]); // cspell:ignore untokenized +function isActiveToken(token: GoogleToken) { + return activeTokenStates.has(token.tokenState); +} + +function needsPushToken(token: GoogleToken) { + return pushTokenStates.has(token.tokenState); +} + +function isIssuerToken(token: GoogleToken): token is TokenInfo { + return "issuerTokenId" in token; +} + +function initWallet() { + if (!provisioningSdk) return Promise.reject(new Error("wallet unavailable on web")); + walletInitPromise ??= provisioningSdk.default + .initialize() + .then(() => { + enableWalletDebugLogging(provisioningSdk); + return provisioningSdk; + }) + .catch((error: unknown) => { + walletLog("wallet.initialize.error", { error: summarizeWalletError(error) }); + walletInitPromise = undefined; + throw error; + }); + return walletInitPromise; +} export default function Card() { const toast = useToastController(); @@ -128,6 +320,7 @@ export default function Card() { queryClient.invalidateQueries({ queryKey: ["kyc", "status"], exact: true }).catch(reportError); if (address) refetchMarkets().catch(reportError); if (address && credential) refetchInstalledPlugins().catch(reportError); + if (Platform.OS !== "web" && cardDetails?.lastFour) refetchWalletEligible().catch(reportError); queryClient.refetchQueries({ queryKey }).catch(reportError); }; useTabPress("card", () => { @@ -264,6 +457,369 @@ export default function Card() { }, }); + const [sdk, setSdk] = useState(null); + const [provisioning, setProvisioning] = useState(false); + const walletInFlightRef = useRef(false); + const walletAttemptRef = useRef(undefined); + const { + data: walletEligible, + isPending: isPendingWallet, + refetch: refetchWalletEligible, + } = useQuery({ + queryKey: ["wallet", "eligible", cardDetails?.lastFour], + enabled: Platform.OS !== "web" && cardDetails?.lastFour.length === 4, + queryFn: async () => { + const lastFour = cardDetails?.lastFour; + if (!lastFour || Platform.OS === "web") return hiddenWallet; + walletLog("wallet.eligibility.start", { lastFour }); + const nextWallet = await initWallet(); + if (Platform.OS === "ios") { + try { + const [{ cardId, cardSecret }, available, canAdd, watchPaired] = await Promise.all([ + traceWalletCall( + "apple.provisioning.fetch", + () => + queryClient.fetchQuery<{ cardId: string; cardSecret: string }>({ + queryKey: ["card", "provisioning"], + staleTime: 0, + }), + { reason: "eligibility" }, + ), + traceWalletCall( + "apple.isPassLibraryAvailable", + () => nextWallet.default.ApplePay.isPassLibraryAvailable(), + { + reason: "eligibility", + }, + ), + traceWalletCall("apple.canAddPaymentPass", () => nextWallet.default.ApplePay.canAddPaymentPass(), { + reason: "eligibility", + }), + traceWalletCall("apple.isWatchPaired", () => nextWallet.default.ApplePay.isWatchPaired(), { + reason: "eligibility", + }).catch(() => undefined), + ]); + const cachedPrimaryAccountIdentifier = primaryAccountIdentifiers.get(cardId); + walletLog("apple.eligibility.base", { + available, + canAdd, + cardId, + hasCachedPrimaryAccountIdentifier: cachedPrimaryAccountIdentifier !== undefined, + lastFour, + watchPaired, + }); + if (!available || !canAdd) return hiddenWallet; + const response = + cachedPrimaryAccountIdentifier === undefined + ? await traceWalletCall( + "apple.initializeOemTokenization", + () => + nextWallet.default.ApplePay.initializeOemTokenization( + nextWallet.MppCardDataParameters.withCardSecret(cardId, cardSecret), + ), + { cardId, reason: "eligibility" }, + ) + : { primaryAccountIdentifier: cachedPrimaryAccountIdentifier }; + const primaryAccountIdentifier = response.primaryAccountIdentifier; + if (primaryAccountIdentifier) { + primaryAccountIdentifiers.set(cardId, primaryAccountIdentifier); + walletLog("apple.eligibility.identifier", { + cardId, + lastFour, + primaryAccountIdentifier, + usedCachedPrimaryAccountIdentifier: cachedPrimaryAccountIdentifier !== undefined, + }); + const secureElementPassExists = await traceWalletCall( + "apple.secureElementPassExists", + () => + nextWallet.default.ApplePay.secureElementPassExistsWithPrimaryAccountIdentifier( + primaryAccountIdentifier, + ), + { primaryAccountIdentifier, reason: "eligibility" }, + ); + const [ + canAddByPrimaryAccountIdentifier, + canAddRemoteSecureElement, + canAddSecureElement, + remoteSecureElementPassExists, + ] = await Promise.all([ + traceWalletCall( + "apple.canAddPaymentPassWithPrimaryAccountIdentifier", + () => + nextWallet.default.ApplePay.canAddPaymentPassWithPrimaryAccountIdentifier(primaryAccountIdentifier), + { primaryAccountIdentifier, reason: "eligibility" }, + ).catch(() => undefined), + traceWalletCall( + "apple.canAddRemoteSecureElementPassWithPrimaryAccountIdentifier", + () => + nextWallet.default.ApplePay.canAddRemoteSecureElementPassWithPrimaryAccountIdentifier( + primaryAccountIdentifier, + ), + { primaryAccountIdentifier, reason: "eligibility" }, + ).catch(() => undefined), + traceWalletCall( + "apple.canAddSecureElementPassWithPrimaryAccountIdentifier", + () => + nextWallet.default.ApplePay.canAddSecureElementPassWithPrimaryAccountIdentifier( + primaryAccountIdentifier, + ), + { primaryAccountIdentifier, reason: "eligibility" }, + ).catch(() => undefined), + traceWalletCall( + "apple.remoteSecureElementPassExistsWithPrimaryAccountIdentifier", + () => + nextWallet.default.ApplePay.remoteSecureElementPassExistsWithPrimaryAccountIdentifier( + primaryAccountIdentifier, + ), + { primaryAccountIdentifier, reason: "eligibility" }, + ).catch(() => undefined), + ]); + walletLog("apple.eligibility.diagnostics", { + canAddByPrimaryAccountIdentifier, + canAddRemoteSecureElement, + canAddSecureElement, + lastFour, + remoteSecureElementPassExists, + secureElementPassExists, + }); + return { apple: !secureElementPassExists, google: "hidden", googleToken: null }; + } + walletLog("apple.eligibility.identifier.missing", { + cardId, + keys: Object.keys(response), + lastFour, + primaryAccountSuffix: response.primaryAccountSuffix, + tokenizationReceiptLength: response.tokenizationReceipt?.length, + validFor: response.validFor, + }); + return { apple: true, google: "hidden", googleToken: null }; + } catch (error) { + walletLog("apple.eligibility.error", { error: summarizeWalletError(error), lastFour }); + reportError(error); + return { apple: true, google: "hidden", googleToken: null }; + } + } + if (Platform.OS !== "android") return hiddenWallet; + return nextWallet.default.GooglePay.isWalletAvailable() + .then(async (available) => { + if (!available) return hiddenWallet; + const token = await queryClient + .fetchQuery<{ cardId: string; cardSecret: string }>({ + queryKey: ["card", "provisioning"], + staleTime: 0, + }) + .then(({ cardId, cardSecret }) => + nextWallet.default.GooglePay.checkWalletForCardToken( + nextWallet.MppCardDataParameters.withCardSecret(cardId, cardSecret), + ), + ) + .catch((error: unknown) => { + const code = + typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" + ? error.code + : undefined; + const walletApiCode = + typeof error === "object" && + error !== null && + "userInfo" in error && + typeof error.userInfo === "object" && + error.userInfo !== null && + "code" in error.userInfo && + typeof error.userInfo.code === "number" + ? error.userInfo.code + : undefined; + if (code === "GOOGLE_PAY_TOKEN_NOT_FOUND" || walletApiCode === 702) { + walletLog("google.eligibility.tokenMissing", { code, lastFour, walletApiCode }); + return null; + } + reportError(error); + }); + if (token === undefined) return hiddenWallet; + const tokenList: GoogleToken[] = []; + if (Array.isArray(token)) { + for (const item of token) if (isGoogleToken(item) && !needsPushToken(item)) tokenList.push(item); + } else if (isGoogleToken(token) && !needsPushToken(token)) { + tokenList.push(token); + } + const google: WalletEligibility["google"] = + tokenList.length === 0 ? "cta" : tokenList.some((item) => isActiveToken(item)) ? "added" : "cta"; + return { + apple: false, + google, + googleToken: tokenList.find((item) => !isActiveToken(item)) ?? null, + }; + }) + .catch((error: unknown) => { + reportError(error); + return hiddenWallet; + }); + }, + }); + + useEffect(() => { + if (Platform.OS === "web" || cardDetails?.lastFour.length !== 4) return; + const lastFour = cardDetails.lastFour; + let mounted = true; + let cleanup: (() => void) | undefined; + initWallet() + .then((nextWallet) => { + if (!mounted) return; + setSdk((current) => current ?? nextWallet); + if (Platform.OS === "ios") { + const traceSubscription = getApplePayEmitter()?.addListener("ApplePayTrace", (data) => { + walletLog("apple.native", { lastFour, trace: data }); + }); + const subscription = nextWallet.default.ApplePay.registerDataChangedListener((data) => { + walletLog("apple.dataChanged", { data, lastFour }); + refetchWalletEligible().catch((error: unknown) => { + walletLog("apple.refetchWalletEligible.error", { + error: summarizeWalletError(error), + lastFour, + }); + reportError(error); + }); + }); + cleanup = () => { + traceSubscription?.remove(); + nextWallet.default.ApplePay.removeDataChangedListener(subscription); + }; + return; + } + const subscription = nextWallet.default.GooglePay.registerDataChangedListener(() => { + walletLog("google.dataChanged", { lastFour }); + refetchWalletEligible().catch((error: unknown) => { + walletLog("google.refetchWalletEligible.error", { + error: summarizeWalletError(error), + lastFour, + }); + reportError(error); + }); + }); + cleanup = () => nextWallet.default.GooglePay.removeDataChangedListener(subscription); + }) + .catch((error: unknown) => { + walletLog("wallet.listener.error", { error: summarizeWalletError(error), lastFour }); + }); + return () => { + mounted = false; + cleanup?.(); + }; + }, [cardDetails?.lastFour, refetchWalletEligible]); + + const withWalletProvisioning = (work: () => Promise) => { + const lastFour = cardDetails?.lastFour; + if (walletInFlightRef.current) { + walletLog("wallet.provisioning.ignored", { lastFour, reason: "in_flight" }); + return Promise.resolve(); + } + const attempt = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + const startedAt = Date.now(); + walletAttemptRef.current = attempt; + walletInFlightRef.current = true; + setProvisioning(true); + walletLog("wallet.provisioning.start", { attempt, lastFour }); + return work() + .then((result) => { + walletLog("wallet.provisioning.success", { + attempt, + durationMs: Date.now() - startedAt, + lastFour, + }); + return result; + }) + .catch((error: unknown) => { + const classification = classifyError(error); + walletLog("wallet.provisioning.error", { + attempt, + durationMs: Date.now() - startedAt, + error: summarizeWalletError(error), + lastFour, + }); + if (classification.walletCancelled) { + walletLog("wallet.provisioning.cancelled", { + attempt, + durationMs: Date.now() - startedAt, + lastFour, + }); + return; + } + reportError(error); + throw error; + }) + .finally(() => { + walletLog("wallet.provisioning.finish", { + attempt, + durationMs: Date.now() - startedAt, + lastFour, + }); + walletAttemptRef.current = undefined; + walletInFlightRef.current = false; + setProvisioning(false); + }); + }; + + const addToAppleWallet = () => + withWalletProvisioning(async () => { + walletLog("apple.add.start", { hasSdk: sdk !== null, lastFour: cardDetails?.lastFour }); + const nextWallet = sdk ?? (await initWallet()); + const { cardId, cardSecret } = await traceWalletCall( + "apple.provisioning.fetch", + () => + queryClient.fetchQuery<{ cardId: string; cardSecret: string }>({ + queryKey: ["card", "provisioning"], + staleTime: 0, + }), + { reason: "add" }, + ); + const response = await traceWalletCall( + "apple.initializeOemTokenization", + () => + nextWallet.default.ApplePay.initializeOemTokenization( + nextWallet.MppCardDataParameters.withCardSecret(cardId, cardSecret), + ), + { cardId, reason: "add" }, + ); + walletLog("apple.add.tokenization", { + cardId, + hasPrimaryAccountIdentifier: response.primaryAccountIdentifier !== undefined, + keys: Object.keys(response), + primaryAccountIdentifier: response.primaryAccountIdentifier, + primaryAccountSuffix: response.primaryAccountSuffix, + tokenizationReceiptLength: response.tokenizationReceipt?.length, + validFor: response.validFor, + }); + const activationState = await traceWalletCall( + "apple.showAddPaymentPassView", + () => nextWallet.default.ApplePay.showAddPaymentPassView(response), + { cardId, primaryAccountIdentifier: response.primaryAccountIdentifier, validFor: response.validFor }, + ); + walletLog("apple.add.result", { activationState, cardId, lastFour: cardDetails?.lastFour }); + return true; + }); + + const addToGoogleWallet = () => + withWalletProvisioning(async () => { + const nextWallet = sdk ?? (await initWallet()); + const googleToken = walletEligible?.googleToken; + if (googleToken) { + await (isIssuerToken(googleToken) + ? nextWallet.default.GooglePay.activateWithTokenInfo(googleToken) + : nextWallet.default.GooglePay.tokenize(googleToken, cardDetails?.displayName ?? "")); + return true; + } + const { cardId, cardSecret } = await queryClient.fetchQuery<{ cardId: string; cardSecret: string }>({ + queryKey: ["card", "provisioning"], + staleTime: 0, + }); + await nextWallet.default.GooglePay.push( + nextWallet.MppCardDataParameters.withCardSecret(cardId, cardSecret), + cardDetails?.displayName ?? "", + {}, + ); + return true; + }); + + const AddPassButton = Platform.OS === "ios" ? sdk?.default.ApplePay.AddPassButton : undefined; const displayStatus = isSettingCardStatus ? optimisticCardStatus : cardDetails?.status; return ( @@ -320,6 +876,65 @@ export default function Card() { revealCard().catch(reportError); }} /> + {Platform.OS !== "web" && + cardDetails && + !isPendingWallet && + walletEligible && + (walletEligible.apple || walletEligible.google !== "hidden") ? ( + + {provisioning ? ( + + ) : ( + <> + {AddPassButton && walletEligible.apple ? ( + { + addToAppleWallet() + .then((added) => { + if (!added) return; + Alert.alert( + t("Card added"), + t("Your card was added to your wallet. Follow any remaining steps if prompted."), + ); + }) + .catch(() => undefined); + }} + /> + ) : null} + {walletEligible.google === "cta" ? ( + { + addToGoogleWallet() + .then((added) => { + if (!added) return; + Alert.alert( + t("Card added"), + t("Your card was added to your wallet. Follow any remaining steps if prompted."), + ); + }) + .catch(() => undefined); + }} + > + + + ) : null} + {walletEligible.google === "added" ? ( + + + + {t("added to google wallet")} + + + ) : null} + + )} + + ) : null} void; op const theme = useColorScheme(); const toast = useToastController(); const { t } = useTranslation(); - const { data: alertShown } = useQuery({ queryKey: ["settings", "alertShown"] }); const { data: card, isPending } = useQuery({ queryKey: ["card", "details"] }); + const { data: alertShown } = useQuery({ queryKey: ["settings", "alertShown"] }); const [details, setDetails] = useState({ pan: "", cvc: "" }); useEffect(() => { if (card?.encryptedPan && card.encryptedCvc) { @@ -171,7 +171,8 @@ export default function CardDetails({ open, onClose }: { onClose: () => void; op ) : null} - {card && alertShown ? ( + + {Platform.OS === "web" && card && alertShown ? ( { @@ -179,6 +180,7 @@ export default function CardDetails({ open, onClose }: { onClose: () => void; op }} /> ) : null} + diff --git a/src/i18n/en.json b/src/i18n/en.json index 7f3f3f989..6301f4bdc 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -14,5 +14,6 @@ "Pending requests → {{count}}_one": "Pending request → {{count}}", "Pending requests → {{count}}_other": "Pending requests → {{count}}", "You repay {{count}} installments of_one": "You repay {{count}} installment of", - "You repay {{count}} installments of_other": "You repay {{count}} installments of" + "You repay {{count}} installments of_other": "You repay {{count}} installments of", + "added to google wallet": "added to google wallet" } diff --git a/src/i18n/es-AR.json b/src/i18n/es-AR.json index fbeadc018..3f52c2752 100644 --- a/src/i18n/es-AR.json +++ b/src/i18n/es-AR.json @@ -128,6 +128,8 @@ "You must repay each installment manually before its due date.": "Debés pagar cada cuota manualmente antes de su fecha de vencimiento.", "You send": "Enviás", "Your card is awaiting activation. Follow the steps to enable it.": "Tu tarjeta está a la espera de activación. Seguí los pasos para habilitarla.", + "Card added": "Tarjeta agregada", + "Your card was added to your wallet. Follow any remaining steps if prompted.": "Tu tarjeta fue agregada a tu billetera. Seguí cualquier paso restante si se te indica.", "Your password manager does not support passkey backups. Please try a different one": "Tu gestor de contraseñas no admite copias de seguridad de llaves de acceso. Por favor, probá con otro.", "Your spending limit is the maximum amount you can spend on your Exa Card.": "Tu límite de gasto es el monto máximo que podés gastar con tu Exa Card.", "Your transactions will show up here once you get started. Add funds to begin!": "Tus transacciones aparecerán aquí una vez que comiences. ¡Agregá fondos para comenzar!", diff --git a/src/i18n/es.json b/src/i18n/es.json index 62973a661..f125f5743 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -126,6 +126,7 @@ "Card number copied!": "¡Número de tarjeta copiado!", "Card upgraded successfully": "Tarjeta actualizada exitosamente", "Card": "Tarjeta", + "added to google wallet": "agregado a google wallet", "Chain {{id}}": "Cadena {{id}}", "Change the pay mode before each purchase and pay how you want.": "Cambia el modo de pago antes de cada compra y paga como quieras.", "Check out our X or Discord for updates—or report the issue so we can take a closer look.": "Revisa nuestro X o Discord para actualizaciones o reporta el problema para que podamos investigarlo más a fondo.", @@ -697,6 +698,8 @@ "Your Exa account": "Tu cuenta Exa", "Your Exa Card is now upgraded to Visa Signature.": "Tu Exa Card ha sido actualizada a Visa Signature.", "Your funds serve as collateral to increase your spending limits.": "Tus fondos sirven como garantía para aumentar tus límites de gasto.", + "Card added": "Tarjeta agregada", + "Your card was added to your wallet. Follow any remaining steps if prompted.": "Tu tarjeta fue agregada a tu billetera. Sigue cualquier paso restante si se te indica.", "Your funds serve as collateral, increasing your spending limits. The more funds you add, the more you can spend with the Exa Card.": "Tus fondos sirven como garantía, aumentando tus límites de gasto. Cuantos más fondos agregues, más podrás gastar con la Exa Card.", "Your ID needs to be updated": "Tu documento necesita ser actualizado", "Your password manager does not support passkey backups. Please try a different one": "Tu gestor de contraseñas no admite copias de seguridad de llaves de acceso. Por favor, prueba con otro.", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 53ba8bef2..b335cb8a3 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -126,6 +126,7 @@ "Card number copied!": "Número do cartão copiado!", "Card upgraded successfully": "Cartão atualizado com sucesso", "Card": "Cartão", + "added to google wallet": "adicionado ao google wallet", "Chain {{id}}": "Rede {{id}}", "Change the pay mode before each purchase and pay how you want.": "Mude o modo de pagamento antes de cada compra e pague como quiser.", "Check out our X or Discord for updates—or report the issue so we can take a closer look.": "Confira nosso X ou Discord para atualizações — ou reporte o problema para que possamos investigar mais a fundo.", @@ -697,6 +698,8 @@ "Your Exa account": "Sua conta Exa", "Your Exa Card is now upgraded to Visa Signature.": "Seu Exa Card foi atualizado para Visa Signature.", "Your funds serve as collateral to increase your spending limits.": "Seus fundos servem como garantia para aumentar seus limites de gastos.", + "Card added": "Cartão adicionado", + "Your card was added to your wallet. Follow any remaining steps if prompted.": "Seu cartão foi adicionado à sua carteira. Siga qualquer etapa restante se for solicitado.", "Your funds serve as collateral, increasing your spending limits. The more funds you add, the more you can spend with the Exa Card.": "Seus fundos servem como garantia, aumentando seus limites de gastos. Quanto mais fundos você adicionar, mais poderá gastar com o Exa Card.", "Your ID needs to be updated": "Seu documento precisa ser atualizado", "Your password manager does not support passkey backups. Please try a different one": "Seu gerenciador de senhas não suporta backup de chaves de acesso. Por favor, tente outro", diff --git a/src/utils/reportError.ts b/src/utils/reportError.ts index 41bea024f..0320fe60d 100644 --- a/src/utils/reportError.ts +++ b/src/utils/reportError.ts @@ -78,6 +78,14 @@ function parseError(error: unknown) { typeof root === "object" && root !== null && "code" in root && typeof root.code === "string" && root.code.length > 0 ? root.code : undefined; + let domain = + typeof root === "object" && + root !== null && + "domain" in root && + typeof root.domain === "string" && + root.domain.length > 0 + ? root.domain + : undefined; let status: string | undefined; for ( let cause: unknown = error; @@ -105,12 +113,15 @@ function parseError(error: unknown) { root.message.length > 0 ? normalizeMessage(root.message) : undefined; + const passKit = message?.match(/\bError Domain=([^ ]+) Code=(-?\d+)\b/); + domain ??= passKit?.[1]; + status ??= passKit?.[2]; const revert = error instanceof BaseError && error.walk((r) => r instanceof ContractFunctionRevertedError) !== null; const reason = revertReason(error, { fallback: "unknown" }); - return { code, message, name, reason, revert, status }; + return { code, domain, message, name, reason, revert, status }; } -function classify({ code, message, name, reason, revert, status }: ParsedError) { +function classify({ code, domain, message, name, reason, revert, status }: ParsedError) { const passkeyNotAllowed = name === "NotAllowedError" || (message !== undefined && authPrefixes.some((prefix) => message.startsWith(prefix))); const passkeyCancelled = @@ -122,6 +133,7 @@ function classify({ code, message, name, reason, revert, status }: ParsedError) (passkeyKnownPatterns.some((pattern) => pattern.test(message)) || authPrefixes.some((prefix) => message.startsWith(prefix)))); const passkeyWarning = passkeyKnown && !passkeyCancelled && !passkeyNotAllowed; + const walletCancelled = domain === "PKPassKitErrorDomain" && (status === "1" || status === "2"); const biometric = code === "ERR_BIOMETRIC"; const walletRejected = status === "4001" || status === "5000"; const bundleCancelled = status === "5730"; @@ -131,10 +143,16 @@ function classify({ code, message, name, reason, revert, status }: ParsedError) biometric || walletRejected || bundleCancelled || + walletCancelled || message === "invalid operation"; const network = classifyNetwork(message); const knownWarning = - passkeyKnown || biometric || walletRejected || bundleCancelled || message === "invalid operation"; + passkeyKnown || + walletCancelled || + biometric || + walletRejected || + bundleCancelled || + message === "invalid operation"; const knownInfo = network !== undefined; const known = knownWarning || knownInfo; const value = @@ -164,6 +182,7 @@ function classify({ code, message, name, reason, revert, status }: ParsedError) passkeyNotAllowed, passkeyWarning, revert, + walletCancelled, walletRejected, }; } diff --git a/src/utils/server.ts b/src/utils/server.ts index 230ab5c6b..c0aaedc71 100644 --- a/src/utils/server.ts +++ b/src/utils/server.ts @@ -100,6 +100,21 @@ async function getCard() { queryClient.setQueryDefaults(["card", "details"], { queryFn: getCard }); export type CardDetails = Awaited>; +async function getCardProvisioning() { + await auth(); + const { id } = await session(); + const response = await api.card.$get({ header: { sessionid: id }, query: { scope: "provisioning" } }); + if (!response.ok) { + const { code } = await response.json(); + throw new APIError(response.status, code); + } + const card = await parseResponse(response); + if (!card.provisioning) throw new Error("bad card provisioning response"); + return { cardId: card.provisioning.id, cardSecret: card.provisioning.secret }; +} + +queryClient.setQueryDefaults(["card", "provisioning"], { queryFn: getCardProvisioning }); + async function getPIN() { const result = await getCard(); if (!result) return null;