From 1fd9c2ee7d8aee7ecb9979b4c4eb984a5bab91cc Mon Sep 17 00:00:00 2001 From: lmcmz Date: Mon, 5 Jan 2026 18:25:36 +1100 Subject: [PATCH] feat: add key rotation example --- FRW.xcodeproj/project.pbxproj | 15 +++++ FRW/Foundation/Bridge/NativeEventModels.swift | 38 +++++++++++++ .../Bridge/NativeRequestEventEmitter.h | 5 ++ .../Bridge/NativeRequestEventEmitter.m | 51 +++++++++++++++++ FRW/Foundation/Bridge/RCTNativeFRWBridge.mm | 52 ++++++++++++++++++ FRW/Foundation/Bridge/TurboModuleSwift.swift | 33 +++++++++++ .../Define/NotificationDefine.swift | 2 + .../Manager/Wallet/WalletManager.swift | 55 +++++++++++++++++++ 8 files changed, 251 insertions(+) create mode 100644 FRW/Foundation/Bridge/NativeEventModels.swift create mode 100644 FRW/Foundation/Bridge/NativeRequestEventEmitter.h create mode 100644 FRW/Foundation/Bridge/NativeRequestEventEmitter.m diff --git a/FRW.xcodeproj/project.pbxproj b/FRW.xcodeproj/project.pbxproj index d8de885f..c1f981ad 100644 --- a/FRW.xcodeproj/project.pbxproj +++ b/FRW.xcodeproj/project.pbxproj @@ -388,6 +388,8 @@ 1570467F2793EBA100D1747B /* GithubEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1570467D2793EBA100D1747B /* GithubEndpoint.swift */; }; 1575765E2E322E6A00D7D8C4 /* BridgeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1575765D2E322E6A00D7D8C4 /* BridgeModels.swift */; }; 1575765F2E322E6A00D7D8C4 /* BridgeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1575765D2E322E6A00D7D8C4 /* BridgeModels.swift */; }; + 15C0A7F12F1B123400ABCDEF /* NativeEventModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15C0A7F02F1B123400ABCDEF /* NativeEventModels.swift */; }; + 15C0A7F22F1B123400ABCDEF /* NativeEventModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15C0A7F02F1B123400ABCDEF /* NativeEventModels.swift */; }; 1575A71428B25EE800ADC513 /* FluidView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1575A71328B25EE800ADC513 /* FluidView.swift */; }; 1575A71528B25EE800ADC513 /* FluidView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1575A71328B25EE800ADC513 /* FluidView.swift */; }; 1575A72628B26B9600ADC513 /* index.html in Resources */ = {isa = PBXBuildFile; fileRef = 1575A72128B26B9600ADC513 /* index.html */; }; @@ -479,7 +481,9 @@ 159F7FC42E96B78600B153A2 /* AlertOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 159F7FC32E96B78600B153A2 /* AlertOverlayView.swift */; }; 159F7FC52E96B78600B153A2 /* AlertOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 159F7FC32E96B78600B153A2 /* AlertOverlayView.swift */; }; 15A87E8E2E26D9CF00F0E550 /* RCTNativeFRWBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = 15A87E8D2E26D9CF00F0E550 /* RCTNativeFRWBridge.mm */; }; + 4203AA4AEF9F48688E5A18AF /* NativeRequestEventEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 74AC00DD79D444B3ACA2B5CA /* NativeRequestEventEmitter.m */; }; 15A87E8F2E26D9CF00F0E550 /* RCTNativeFRWBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = 15A87E8D2E26D9CF00F0E550 /* RCTNativeFRWBridge.mm */; }; + F128232BA9CC4A8E8C06C0EA /* NativeRequestEventEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 74AC00DD79D444B3ACA2B5CA /* NativeRequestEventEmitter.m */; }; 15A87E912E26D9E100F0E550 /* TurboModuleSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15A87E902E26D9E100F0E550 /* TurboModuleSwift.swift */; }; 15A87E922E26D9E100F0E550 /* TurboModuleSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15A87E902E26D9E100F0E550 /* TurboModuleSwift.swift */; }; 15ADAE2A28F51EBB0014B722 /* SymmetricEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15ADAE2928F51EBB0014B722 /* SymmetricEncryption.swift */; }; @@ -2715,6 +2719,7 @@ 1570467A2793D60000D1747B /* NFTListResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFTListResponse.swift; sourceTree = ""; }; 1570467D2793EBA100D1747B /* GithubEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubEndpoint.swift; sourceTree = ""; }; 1575765D2E322E6A00D7D8C4 /* BridgeModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeModels.swift; sourceTree = ""; }; + 15C0A7F02F1B123400ABCDEF /* NativeEventModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeEventModels.swift; sourceTree = ""; }; 1575A71328B25EE800ADC513 /* FluidView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FluidView.swift; sourceTree = ""; }; 1575A72128B26B9600ADC513 /* index.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = index.html; sourceTree = ""; }; 1575A72228B26B9600ADC513 /* dat.gui.min.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = dat.gui.min.js; sourceTree = ""; }; @@ -2760,6 +2765,8 @@ 158CE8872782DAD7006A8394 /* UserResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserResponses.swift; sourceTree = ""; }; 158CE88A2782F1FA006A8394 /* RecoveryPhraseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseViewModel.swift; sourceTree = ""; }; 1594792C2E282F7000F2B7A6 /* RCTNativeFRWBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTNativeFRWBridge.h; sourceTree = ""; }; + 2462FD6909FE440094144F44 /* NativeRequestEventEmitter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NativeRequestEventEmitter.h; sourceTree = ""; }; + 74AC00DD79D444B3ACA2B5CA /* NativeRequestEventEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NativeRequestEventEmitter.m; sourceTree = ""; }; 1596A8CF2E1AD76200E85303 /* ReactNativeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactNativeViewController.swift; sourceTree = ""; }; 1596A8D72E1AD9B300E85303 /* main.jsbundle */ = {isa = PBXFileReference; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; 1599F76F278892680094B196 /* InputMnemonicViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputMnemonicViewModel.swift; sourceTree = ""; }; @@ -4840,8 +4847,12 @@ 4E33F7D42E42423F00B3D62F /* RNTokenModel.swift */, 4EECEB202E323F29008E21B6 /* RNEncoder.swift */, 1575765D2E322E6A00D7D8C4 /* BridgeModels.swift */, + 15C0A7F02F1B123400ABCDEF /* NativeEventModels.swift */, 15A87E902E26D9E100F0E550 /* TurboModuleSwift.swift */, 15A87E8D2E26D9CF00F0E550 /* RCTNativeFRWBridge.mm */, + 2462FD6909FE440094144F44 /* NativeRequestEventEmitter.h */, + 74AC00DD79D444B3ACA2B5CA /* NativeRequestEventEmitter.m */, + 1594792C2E282F7000F2B7A6 /* RCTNativeFRWBridge.h */, 4EECEB1D2E3235AB008E21B6 /* NativeToRNModel.swift */, 4E7712E02E3B2AD70011C1DA /* RNBridgeError.swift */, @@ -8820,6 +8831,7 @@ 15DC20B527819C56000B187A /* VChevronButtonDirection.swift in Sources */, 15DC20CF27819C56000B187A /* VNavigationLinkExtension.swift in Sources */, 15A87E8F2E26D9CF00F0E550 /* RCTNativeFRWBridge.mm in Sources */, + F128232BA9CC4A8E8C06C0EA /* NativeRequestEventEmitter.m in Sources */, 15DC212727819C56000B187A /* VSpinnerModelContinous.swift in Sources */, 6AF95B3D2848686D0012C837 /* AddressBookRequest.swift in Sources */, 4E7153212E9752D700C5D0BA /* DeleteSEKeychain.swift in Sources */, @@ -8967,6 +8979,7 @@ 15DC20BD27819C56000B187A /* VSquareButtonModel.swift in Sources */, 4EC482D3284A3D75000A7E1B /* MMCQ.swift in Sources */, 1575765F2E322E6A00D7D8C4 /* BridgeModels.swift in Sources */, + 15C0A7F22F1B123400ABCDEF /* NativeEventModels.swift in Sources */, 15DC21AB27819C56000B187A /* FocusableTextField.swift in Sources */, 15DC219B27819C56000B187A /* VTextFieldActions.swift in Sources */, 6A98509C28C71B5D00A07C2D /* BrowserAuthnViewModel.swift in Sources */, @@ -9911,6 +9924,7 @@ 15C58B0D2868A4EE00BD4FC6 /* VNavigationLinkExtension.swift in Sources */, 15C58B0E2868A4EE00BD4FC6 /* VSpinnerModelContinous.swift in Sources */, 15A87E8E2E26D9CF00F0E550 /* RCTNativeFRWBridge.mm in Sources */, + 4203AA4AEF9F48688E5A18AF /* NativeRequestEventEmitter.m in Sources */, 15C58B0F2868A4EE00BD4FC6 /* AddressBookRequest.swift in Sources */, 15C58B102868A4EE00BD4FC6 /* VMenuRow.swift in Sources */, 4E7153222E9752D700C5D0BA /* DeleteSEKeychain.swift in Sources */, @@ -10058,6 +10072,7 @@ 15C58B512868A4EE00BD4FC6 /* FocusableTextField.swift in Sources */, 15C58B522868A4EE00BD4FC6 /* VTextFieldActions.swift in Sources */, 1575765E2E322E6A00D7D8C4 /* BridgeModels.swift in Sources */, + 15C0A7F12F1B123400ABCDEF /* NativeEventModels.swift in Sources */, 6A98509B28C71B5D00A07C2D /* BrowserAuthnViewModel.swift in Sources */, 6A73BF5228C59028004B836A /* JSMessageHandler.swift in Sources */, 4EE6FA892B19677B006A827B /* SyncConfirmViewModel.swift in Sources */, diff --git a/FRW/Foundation/Bridge/NativeEventModels.swift b/FRW/Foundation/Bridge/NativeEventModels.swift new file mode 100644 index 00000000..865d34af --- /dev/null +++ b/FRW/Foundation/Bridge/NativeEventModels.swift @@ -0,0 +1,38 @@ +// +// NativeEventModels.swift +// FRW +// +// Auto-generated from TypeScript bridge types +// Do not edit manually +// + +import Foundation + +enum RNNativeEvent { + struct KeyRotationCheckParams: Codable { + let address: String + } + + struct KeyRotationCheckResult: Codable { + let address: String + let isBlocto: Bool + } + + struct NativeRequestPayload: Codable { + let requestId: String + let eventName: NativeEventName + let paramsJson: String + } + + struct NativeResponsePayload: Codable { + let requestId: String + let eventName: NativeEventName + let resultJson: String? + let error: String? + } + + enum NativeEventName: String, Codable { + case keyrotationcheck = "keyRotationCheck" + } + +} diff --git a/FRW/Foundation/Bridge/NativeRequestEventEmitter.h b/FRW/Foundation/Bridge/NativeRequestEventEmitter.h new file mode 100644 index 00000000..972057a6 --- /dev/null +++ b/FRW/Foundation/Bridge/NativeRequestEventEmitter.h @@ -0,0 +1,5 @@ +#import +#import + +@interface NativeRequestEventEmitter : RCTEventEmitter +@end diff --git a/FRW/Foundation/Bridge/NativeRequestEventEmitter.m b/FRW/Foundation/Bridge/NativeRequestEventEmitter.m new file mode 100644 index 00000000..6b2fa540 --- /dev/null +++ b/FRW/Foundation/Bridge/NativeRequestEventEmitter.m @@ -0,0 +1,51 @@ +#import "NativeRequestEventEmitter.h" + +@interface NativeRequestEventEmitter () +@property (nonatomic, assign) BOOL hasListeners; +@end + +@implementation NativeRequestEventEmitter + +RCT_EXPORT_MODULE(); + +- (NSArray *)supportedEvents { + return @[ @"nativeRequest" ]; +} + +- (void)startObserving { + self.hasListeners = YES; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleNativeRequest:) + name:@"nativeRequest" + object:nil]; +} + +- (void)stopObserving { + self.hasListeners = NO; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:@"nativeRequest" + object:nil]; +} + +- (void)invalidate { + [super invalidate]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)handleNativeRequest:(NSNotification *)notification { + if (!self.hasListeners) { + return; + } + + NSDictionary *userInfo = [notification.userInfo isKindOfClass:[NSDictionary class]] + ? notification.userInfo + : @{}; + + if (userInfo.count == 0) { + return; + } + + [self sendEventWithName:@"nativeRequest" body:userInfo]; +} + +@end diff --git a/FRW/Foundation/Bridge/RCTNativeFRWBridge.mm b/FRW/Foundation/Bridge/RCTNativeFRWBridge.mm index cda89d8d..d62439bd 100644 --- a/FRW/Foundation/Bridge/RCTNativeFRWBridge.mm +++ b/FRW/Foundation/Bridge/RCTNativeFRWBridge.mm @@ -108,6 +108,58 @@ - (void)sign:(nonnull NSString *)hexData }]; } +- (void)signRotationRequest:(nonnull NSString *)publicKey + address:(nonnull NSString *)address + hash:(nonnull NSString *)hash + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [TurboModuleSwift signRotationRequestWithPublicKey:publicKey + address:address + hash:hash + completionHandler:^(NSString *_Nullable signature, + NSError *_Nullable error) { + if (error) { + reject(@"sign_rotation_error", error.localizedDescription, error); + } else { + resolve(signature); + } + }]; +} + +- (void)removeOldKey:(nonnull NSString *)address + publicKey:(nonnull NSString *)publicKey + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [TurboModuleSwift removeOldKeyWithAddress:address + publicKey:publicKey + completionHandler:^(NSError *_Nullable error) { + if (error) { + reject(@"remove_old_key_error", error.localizedDescription, error); + } else { + resolve(nil); + } + }]; +} + +- (void)nativeResponse:(nonnull NSString *)requestId + eventName:(nonnull NSString *)eventName + resultJson:(NSString * _Nullable)resultJson + error:(NSString * _Nullable)error + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + [TurboModuleSwift nativeResponseWithRequestId:requestId + eventName:eventName + resultJson:resultJson + error:error + completionHandler:^(NSError *_Nullable nativeError) { + if (nativeError) { + reject(@"native_response_error", nativeError.localizedDescription, nativeError); + } else { + resolve(nil); + } + }]; +} + - (NSNumber *)getSignKeyIndex { return @([TurboModuleSwift getSignKeyIndex]); } diff --git a/FRW/Foundation/Bridge/TurboModuleSwift.swift b/FRW/Foundation/Bridge/TurboModuleSwift.swift index 779def2a..e5b9fd4d 100644 --- a/FRW/Foundation/Bridge/TurboModuleSwift.swift +++ b/FRW/Foundation/Bridge/TurboModuleSwift.swift @@ -151,6 +151,39 @@ extension TurboModuleSwift { "INSTABUG_TOKEN": ServiceConfig.instabugRNToken, ] } + + @objc + static func signRotationRequest(publicKey: String, address: String, hash: String) async throws -> String { + _ = publicKey + _ = address + let data = Data(hash.utf8) + return try await WalletManager.shared.sign(signableData: data).hexString + } + + @objc + static func removeOldKey(address: String, publicKey: String) async throws { + _ = address + _ = publicKey + } + + @objc + static func nativeResponse( + requestId: String, + eventName: String, + resultJson: String?, + error: String? + ) async throws { + NotificationCenter.default.post( + name: .nativeResponse, + object: nil, + userInfo: [ + "requestId": requestId, + "eventName": eventName, + "resultJson": resultJson ?? "", + "error": error ?? "", + ] + ) + } } // MARK: - React Native Management diff --git a/FRW/Foundation/Define/NotificationDefine.swift b/FRW/Foundation/Define/NotificationDefine.swift index b42aa34d..6f2bdabf 100644 --- a/FRW/Foundation/Define/NotificationDefine.swift +++ b/FRW/Foundation/Define/NotificationDefine.swift @@ -46,4 +46,6 @@ public extension Notification.Name { static let accountDataDidUpdate = Notification.Name("accountDataDidUpdate") static let dropboxCallback = Notification.Name("multiBack.dropboxCallback") static let hiddenAddressesDidChanged = Notification.Name("hiddenAddressesDidChanged") + static let nativeRequest = Notification.Name("nativeRequest") + static let nativeResponse = Notification.Name("nativeResponse") } diff --git a/FRW/Services/Manager/Wallet/WalletManager.swift b/FRW/Services/Manager/Wallet/WalletManager.swift index bd808c06..adc70a7a 100644 --- a/FRW/Services/Manager/Wallet/WalletManager.swift +++ b/FRW/Services/Manager/Wallet/WalletManager.swift @@ -61,6 +61,12 @@ class WalletManager: ObservableObject { name: .willResetWallet, object: nil ) + NotificationCenter.default.addObserver( + self, + selector: #selector(handleNativeResponse(_:)), + name: .nativeResponse, + object: nil + ) self.currentNetwork = LocalUserDefaults.shared.network flow.configure(chainID: currentNetwork) start() @@ -437,6 +443,8 @@ extension WalletManager { mainAccount = account loadLinkedAccounts() } + + notifyKeyRotationAddressChanged(address: address) } func switchSelectedAccount(_ selectingAccount: WalletAccount) { @@ -472,6 +480,8 @@ extension WalletManager { break } + + notifyKeyRotationAddressChanged(address: selectingAccount.address) } func changeNetwork(_ network: Flow.ChainID) { @@ -505,6 +515,51 @@ extension WalletManager { } } +// MARK: - React Native Key Rotation Hook + +extension WalletManager { + private func notifyKeyRotationAddressChanged(address: String) { + guard !address.isEmpty else { return } + let paramsJson: String = { + let payload = ["address": address] + guard let data = try? JSONSerialization.data(withJSONObject: payload), + let json = String(data: data, encoding: .utf8) else { + return "{}" + } + return json + }() + // React Native should observe this notification and trigger KeyRotationService. + NotificationCenter.default.post( + name: .nativeRequest, + object: nil, + userInfo: [ + "requestId": UUID().uuidString, + "eventName": "keyRotationCheck", + "paramsJson": paramsJson, + ] + ) + log.debug("[WalletManager] Posted key rotation address change: \(address)") + } + + @objc + private func handleNativeResponse(_ notification: Notification) { + guard let info = notification.userInfo else { return } + let requestId = info["requestId"] as? String ?? "" + let eventName = info["eventName"] as? String ?? "" + let resultJson = info["resultJson"] as? String ?? "" + let error = info["error"] as? String + log.debug( + "[WalletManager] Native response", + [ + "requestId": requestId, + "eventName": eventName, + "resultJson": resultJson, + "error": error ?? "", + ] + ) + } +} + // MARK: - account type extension WalletManager {