-
Notifications
You must be signed in to change notification settings - Fork 1
feat: support RN app migration #290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds support for migrating existing React Native Bitkit wallets to the native Swift app. The migration automatically detects RN wallet data on first launch and transfers wallet credentials, Lightning channels, settings, activities, widgets, and metadata to the native app format.
Key Changes:
- Comprehensive RN wallet data detection and migration system with MMKV binary parsing
- Channel migration support for Lightning Network state preservation
- Migration loading UI with success/error toast notifications
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| Bitkit/Services/MigrationsService.swift | Implements complete RN migration logic including MMKV parsing, keychain reading, and data transformation for settings, activities, channels, and widgets |
| Bitkit/AppScene.swift | Adds migration detection flow on app startup and displays loading screen during migration |
| Bitkit/ViewModels/WalletViewModel.swift | Passes pending channel migration data to Lightning service during wallet initialization |
| Bitkit/ViewModels/AppViewModel.swift | Handles post-sync migration completion with metadata reapplication and success notification |
| Bitkit/Services/LightningService.swift | Accepts and applies channel migration data during Lightning node setup |
| Bitkit/Services/CoreService.swift | Adds utility to mark all unseen activities as seen after migration |
| Bitkit/Utilities/Keychain.swift | Removes outdated TODO comment about RN keychain migration |
| Bitkit.xcodeproj/.../Package.resolved | Updates ldk-node dependency revision for channel migration support |
| var jsonString = rootJson | ||
| if let jsonStart = rootJson.firstIndex(of: "{") { | ||
| jsonString = String(rootJson[jsonStart...]) | ||
| } |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JSON extraction logic is duplicated across multiple extract methods (extractRNMetadata, extractRNWidgets, extractRNActivities, extractRNClosedChannels). Consider extracting this into a shared helper method that takes a key name and returns the parsed JSON object.
| var activityId = txId | ||
| if let onchain = try? await CoreService.shared.activity.getOnchainActivityByTxId(txid: txId) { | ||
| activityId = onchain.id | ||
| } |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This pattern of converting txId to activityId is duplicated in applyRNMetadata (lines 767-770, 787-790) and applyOnchainMetadata (lines 985-986). Extract this into a helper method to reduce duplication and improve maintainability.
| func getBool(from dict: [String: Any], key: String, fallbackKey: String? = nil, defaultValue: Bool) -> Bool { | ||
| let keys = fallbackKey != nil ? [key, fallbackKey!] : [key] | ||
| for k in keys { | ||
| if let val = dict[k] as? Bool { return val } | ||
| if let val = dict[k] as? Int { return val != 0 } | ||
| if let val = dict[k] as? NSNumber { return val.boolValue } | ||
| } | ||
| return defaultValue | ||
| } |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This nested helper function is quite useful and handles different boolean representations. Consider extracting it as a private method of MigrationsService so it can be reused elsewhere if needed, and add a comment explaining why multiple type checks are necessary for RN data compatibility.
| try? await CoreService.shared.activity.syncLdkNodePayments(LightningService.shared.payments ?? []) | ||
| await CoreService.shared.activity.markAllUnseenActivitiesAsSeen() | ||
| await MigrationsService.shared.reapplyMetadataAfterSync() | ||
| try? await LightningService.shared.restart() | ||
|
|
||
| MigrationsService.shared.isShowingMigrationLoading = false | ||
| self.toast(type: .success, title: "Migration Complete", description: "Your wallet has been successfully migrated") |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Errors from syncLdkNodePayments and LightningService.restart are silently ignored with try?. If these operations fail during migration, the user will see a success message even though the migration may be incomplete. Consider logging these errors or handling them appropriately, potentially showing a different message if critical steps fail.
| try? await CoreService.shared.activity.syncLdkNodePayments(LightningService.shared.payments ?? []) | |
| await CoreService.shared.activity.markAllUnseenActivitiesAsSeen() | |
| await MigrationsService.shared.reapplyMetadataAfterSync() | |
| try? await LightningService.shared.restart() | |
| MigrationsService.shared.isShowingMigrationLoading = false | |
| self.toast(type: .success, title: "Migration Complete", description: "Your wallet has been successfully migrated") | |
| var hadError = false | |
| do { | |
| let payments = LightningService.shared.payments ?? [] | |
| try await CoreService.shared.activity.syncLdkNodePayments(payments) | |
| } catch { | |
| hadError = true | |
| Logger.warn("Failed to sync LDK node payments during migration: \(error)", context: "AppViewModel") | |
| } | |
| await CoreService.shared.activity.markAllUnseenActivitiesAsSeen() | |
| await MigrationsService.shared.reapplyMetadataAfterSync() | |
| do { | |
| try await LightningService.shared.restart() | |
| } catch { | |
| hadError = true | |
| Logger.warn("Failed to restart LightningService after migration: \(error)", context: "AppViewModel") | |
| } | |
| MigrationsService.shared.isShowingMigrationLoading = false | |
| if hadError { | |
| self.toast( | |
| type: .warning, | |
| title: "Migration Completed With Issues", | |
| description: "Your wallet migration completed, but some background tasks may not have finished correctly." | |
| ) | |
| } else { | |
| self.toast( | |
| type: .success, | |
| title: "Migration Complete", | |
| description: "Your wallet has been successfully migrated" | |
| ) | |
| } |
| var result: [String: Data] = [:] | ||
|
|
||
| func getBool(from dict: [String: Any], key: String, fallbackKey: String? = nil, defaultValue: Bool) -> Bool { | ||
| let keys = fallbackKey != nil ? [key, fallbackKey!] : [key] |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Force unwrapping fallbackKey with ! is unsafe. Use optional unwrapping instead: let keys = [key] + (fallbackKey.map { [$0] } ?? []) or a more explicit check.
| let keys = fallbackKey != nil ? [key, fallbackKey!] : [key] | |
| let keys = [key] + (fallbackKey.map { [$0] } ?? []) |
| channelId: channel.channel_id, | ||
| counterpartyNodeId: channel.counterparty_node_id ?? "", | ||
| fundingTxoTxid: fundingTxid, | ||
| fundingTxoIndex: 0, |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fundingTxoIndex is hardcoded to 0, but RN channels may have used different output indices. The correct index should be extracted from the RN channel data or the funding transaction if available, otherwise channel identification may be incorrect.
| try await ServiceQueue.background(.core) { | ||
| try BitkitCore.markActivityAsSeen(activityId: id, seenAt: timestamp) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe could apply some parallelization in case the wallet has many activities, but I'm not sure if it would make a big difference
This PR adds support for migrating an existing React Native Bitkit into the Bitkit native app.
The migration passes:
record.mp4
What's still needed: