diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index cf03392f..08391578 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -10,6 +10,7 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { implementation project(':capacitor-app') + implementation project(':capacitor-browser') implementation project(':capacitor-filesystem') implementation project(':capacitor-haptics') implementation project(':capacitor-keyboard') diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 544a7541..2ba6607f 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -5,6 +5,9 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/ include ':capacitor-app' project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') +include ':capacitor-browser' +project(':capacitor-browser').projectDir = new File('../node_modules/@capacitor/browser/android') + include ':capacitor-filesystem' project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android') diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index f909bce9..268663a0 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 83CCE39C02FD0BF8DD39551F /* AppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A6B2865F76F213517CFCCD1 /* AppViewController.swift */; }; B54E83DC5BCCDB512256423A /* LocalBiometricPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A34DAD709C71D553F88951 /* LocalBiometricPlugin.swift */; }; C7D4E92A3F8B1C5D00A2B9E1 /* BarcodeScannerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D4E92B3F8B1C5D00A2B9E2 /* BarcodeScannerPlugin.swift */; }; + D1A2B3C4E5F607890A1B2C3D /* PasskeyPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A2B3C4E5F607890A1B2C3E /* PasskeyPlugin.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -36,6 +37,7 @@ 958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; }; BFB20C26958B0AB36D108D0E /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; }; C7D4E92B3F8B1C5D00A2B9E2 /* BarcodeScannerPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BarcodeScannerPlugin.swift; sourceTree = ""; }; + D1A2B3C4E5F607890A1B2C3E /* PasskeyPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PasskeyPlugin.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -81,6 +83,7 @@ 50B271D01FEDC1A000F3C39B /* public */, 21A34DAD709C71D553F88951 /* LocalBiometricPlugin.swift */, C7D4E92B3F8B1C5D00A2B9E2 /* BarcodeScannerPlugin.swift */, + D1A2B3C4E5F607890A1B2C3E /* PasskeyPlugin.swift */, 1A6B2865F76F213517CFCCD1 /* AppViewController.swift */, 873F0344C8952CB5585102E0 /* App.entitlements */, ); @@ -180,6 +183,7 @@ B54E83DC5BCCDB512256423A /* LocalBiometricPlugin.swift in Sources */, C7D4E92A3F8B1C5D00A2B9E1 /* BarcodeScannerPlugin.swift in Sources */, 83CCE39C02FD0BF8DD39551F /* AppViewController.swift in Sources */, + D1A2B3C4E5F607890A1B2C3D /* PasskeyPlugin.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/App/App/App.entitlements b/ios/App/App/App.entitlements index 2a557a66..379accfa 100644 --- a/ios/App/App/App.entitlements +++ b/ios/App/App/App.entitlements @@ -6,5 +6,9 @@ $(AppIdentifierPrefix)com.miden.wallet + com.apple.developer.associated-domains + + webcredentials:api.midenbrowserwallet.com + diff --git a/ios/App/App/AppViewController.swift b/ios/App/App/AppViewController.swift index 7c12db85..4bbaec9b 100644 --- a/ios/App/App/AppViewController.swift +++ b/ios/App/App/AppViewController.swift @@ -5,5 +5,6 @@ class AppViewController: CAPBridgeViewController { override open func capacitorDidLoad() { bridge?.registerPluginInstance(LocalBiometricPlugin()) bridge?.registerPluginInstance(BarcodeScannerPlugin()) + bridge?.registerPluginInstance(PasskeyPlugin()) } } diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index d2810925..8e892bdf 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -51,6 +51,15 @@ UIViewControllerBasedStatusBarAppearance + CFBundleURLTypes + + + CFBundleURLSchemes + + com.googleusercontent.apps.849882985138-gbl44m5nmvuim6eiv4vmtg5rvoq4knqi + + + NSAppTransportSecurity NSAllowsArbitraryLoads diff --git a/ios/App/App/PasskeyPlugin.swift b/ios/App/App/PasskeyPlugin.swift new file mode 100644 index 00000000..7a964a7f --- /dev/null +++ b/ios/App/App/PasskeyPlugin.swift @@ -0,0 +1,278 @@ +import Foundation +import Capacitor +import AuthenticationServices +import CryptoKit +import os.log + +private let logger = OSLog(subsystem: "com.miden.wallet", category: "Passkey") + +/// Native Capacitor plugin for passkey operations using Apple's ASAuthorization API +/// with PRF (Pseudo-Random Function) extension support. +/// +/// WKWebView's JavaScript WebAuthn bridge does not pass through the PRF extension, +/// so we bypass it entirely and call the native API directly. +/// +/// Requires iOS 18.0+ for PRF support. +@objc(PasskeyPlugin) +public class PasskeyPlugin: CAPPlugin, CAPBridgedPlugin, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { + public let identifier = "PasskeyPlugin" + public let jsName = "Passkey" + public let pluginMethods: [CAPPluginMethod] = [ + CAPPluginMethod(name: "isAvailable", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "register", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "authenticate", returnType: CAPPluginReturnPromise) + ] + + // Strong reference to prevent ASAuthorizationController deallocation mid-flow + private var authController: ASAuthorizationController? + private var currentCall: CAPPluginCall? + private var isRegistration = false + + // MARK: - ASAuthorizationControllerPresentationContextProviding + + public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + return self.bridge?.viewController?.view.window ?? ASPresentationAnchor() + } + + // MARK: - Plugin Methods + + @objc func isAvailable(_ call: CAPPluginCall) { + os_log("[Passkey] isAvailable called", log: logger, type: .debug) + if #available(iOS 18.0, *) { + call.resolve(["available": true]) + } else { + os_log("[Passkey] iOS 18.0+ required for PRF support", log: logger, type: .info) + call.resolve(["available": false]) + } + } + + @objc func register(_ call: CAPPluginCall) { + os_log("[Passkey] register called", log: logger, type: .debug) + + guard #available(iOS 18.0, *) else { + call.reject("Passkey PRF requires iOS 18.0+") + return + } + + guard let rpId = call.getString("rpId"), + let userName = call.getString("userName"), + let _ = call.getString("userDisplayName"), + let userIdBase64 = call.getString("userId"), + let challengeBase64 = call.getString("challenge"), + let prfSaltBase64 = call.getString("prfSalt") else { + call.reject("Missing required parameters") + return + } + + guard let userId = Data(base64Encoded: userIdBase64), + let challenge = Data(base64Encoded: challengeBase64), + let prfSalt = Data(base64Encoded: prfSaltBase64) else { + call.reject("Invalid base64 encoding") + return + } + + self.currentCall = call + self.isRegistration = true + + let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId) + let request = provider.createCredentialRegistrationRequest( + challenge: challenge, + name: userName, + userID: userId + ) + + // Attach PRF with salt so registration returns the PRF output directly. + let saltValues = ASAuthorizationPublicKeyCredentialPRFAssertionInput.InputValues(saltInput1: prfSalt) + request.prf = .inputValues(saltValues) + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + let controller = ASAuthorizationController(authorizationRequests: [request]) + controller.delegate = self + controller.presentationContextProvider = self + self.authController = controller + controller.performRequests() + } + } + + @objc func authenticate(_ call: CAPPluginCall) { + os_log("[Passkey] authenticate called", log: logger, type: .debug) + + guard #available(iOS 18.0, *) else { + call.reject("Passkey PRF requires iOS 18.0+") + return + } + + guard let rpId = call.getString("rpId"), + let credentialIdBase64 = call.getString("credentialId"), + let challengeBase64 = call.getString("challenge"), + let prfSaltBase64 = call.getString("prfSalt") else { + call.reject("Missing required parameters") + return + } + + guard let credentialId = Data(base64Encoded: credentialIdBase64), + let challenge = Data(base64Encoded: challengeBase64), + let prfSalt = Data(base64Encoded: prfSaltBase64) else { + call.reject("Invalid base64 encoding") + return + } + + self.currentCall = call + self.isRegistration = false + + let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId) + let request = provider.createCredentialAssertionRequest(challenge: challenge) + + request.allowedCredentials = [ + ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: credentialId) + ] + + let saltValues = ASAuthorizationPublicKeyCredentialPRFAssertionInput.InputValues(saltInput1: prfSalt) + request.prf = .inputValues(saltValues) + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + let controller = ASAuthorizationController(authorizationRequests: [request]) + controller.delegate = self + controller.presentationContextProvider = self + self.authController = controller + controller.performRequests() + } + } + + // MARK: - ASAuthorizationControllerDelegate + + public func authorizationController( + controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization + ) { + os_log("[Passkey] Authorization completed", log: logger, type: .debug) + + guard let call = currentCall else { + os_log("[Passkey] No pending call", log: logger, type: .error) + return + } + + if #available(iOS 18.0, *) { + if let registration = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration { + handleRegistrationResult(registration, call: call) + } else if let assertion = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion { + handleAssertionResult(assertion, call: call) + } else { + call.reject("Unexpected credential type") + cleanup() + } + } else { + call.reject("iOS 18.0+ required") + cleanup() + } + } + + public func authorizationController( + controller: ASAuthorizationController, + didCompleteWithError error: Error + ) { + os_log("[Passkey] Authorization error: %{public}@", log: logger, type: .error, error.localizedDescription) + + guard let call = currentCall else { return } + + let nsError = error as NSError + if nsError.domain == ASAuthorizationError.errorDomain, + let code = ASAuthorizationError.Code(rawValue: nsError.code) { + switch code { + case .canceled: + call.reject("Passkey operation was cancelled", "CANCELLED") + case .failed: + call.reject("Passkey operation failed", "FAILED") + case .invalidResponse: + call.reject("Invalid response from authenticator", "INVALID_RESPONSE") + case .notHandled: + call.reject("Request not handled", "NOT_HANDLED") + case .notInteractive: + call.reject("Not interactive", "NOT_INTERACTIVE") + @unknown default: + call.reject("Authorization error: \(error.localizedDescription)") + } + } else { + call.reject("Passkey error: \(error.localizedDescription)") + } + + cleanup() + } + + // MARK: - Result Handlers + + @available(iOS 18.0, *) + private func handleRegistrationResult( + _ registration: ASAuthorizationPlatformPublicKeyCredentialRegistration, + call: CAPPluginCall + ) { + let credentialId = registration.credentialID + os_log("[Passkey] Registration succeeded, credentialId length: %d", log: logger, type: .debug, credentialId.count) + + guard let prfOutput = registration.prf else { + os_log("[Passkey] No PRF output from registration", log: logger, type: .error) + call.reject("PRF extension not supported by this authenticator") + cleanup() + return + } + + guard prfOutput.isSupported else { + os_log("[Passkey] PRF not supported by authenticator", log: logger, type: .error) + call.reject("PRF extension not supported by this authenticator") + cleanup() + return + } + + guard let prfKey = prfOutput.first else { + os_log("[Passkey] PRF output has no first key", log: logger, type: .error) + call.reject("PRF output not available from registration") + cleanup() + return + } + + let prfData = prfKey.withUnsafeBytes { Data(Array($0)) } + os_log("[Passkey] PRF output obtained from registration, length: %d", log: logger, type: .debug, prfData.count) + + call.resolve([ + "credentialId": credentialId.base64EncodedString(), + "prfOutput": prfData.base64EncodedString() + ]) + + cleanup() + } + + @available(iOS 18.0, *) + private func handleAssertionResult( + _ assertion: ASAuthorizationPlatformPublicKeyCredentialAssertion, + call: CAPPluginCall + ) { + os_log("[Passkey] Assertion completed", log: logger, type: .debug) + + guard let prfResult = assertion.prf else { + os_log("[Passkey] No PRF output in assertion result", log: logger, type: .error) + call.reject("PRF output not available") + cleanup() + return + } + + let prfData = prfResult.first.withUnsafeBytes { Data(Array($0)) } + os_log("[Passkey] PRF output obtained, length: %d", log: logger, type: .debug, prfData.count) + + call.resolve([ + "credentialId": assertion.credentialID.base64EncodedString(), + "prfOutput": prfData.base64EncodedString() + ]) + + cleanup() + } + + // MARK: - Cleanup + + private func cleanup() { + currentCall = nil + authController = nil + isRegistration = false + } +} diff --git a/ios/App/CapApp-SPM/Package.swift b/ios/App/CapApp-SPM/Package.swift index 810263a7..1ae49d7e 100644 --- a/ios/App/CapApp-SPM/Package.swift +++ b/ios/App/CapApp-SPM/Package.swift @@ -13,6 +13,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.0.1"), .package(name: "CapacitorApp", path: "../../../node_modules/@capacitor/app"), + .package(name: "CapacitorBrowser", path: "../../../node_modules/@capacitor/browser"), .package(name: "CapacitorFilesystem", path: "../../../node_modules/@capacitor/filesystem"), .package(name: "CapacitorHaptics", path: "../../../node_modules/@capacitor/haptics"), .package(name: "CapacitorKeyboard", path: "../../../node_modules/@capacitor/keyboard"), @@ -29,6 +30,7 @@ let package = Package( .product(name: "Capacitor", package: "capacitor-swift-pm"), .product(name: "Cordova", package: "capacitor-swift-pm"), .product(name: "CapacitorApp", package: "CapacitorApp"), + .product(name: "CapacitorBrowser", package: "CapacitorBrowser"), .product(name: "CapacitorFilesystem", package: "CapacitorFilesystem"), .product(name: "CapacitorHaptics", package: "CapacitorHaptics"), .product(name: "CapacitorKeyboard", package: "CapacitorKeyboard"), diff --git a/package.json b/package.json index 29a69056..8106134c 100644 --- a/package.json +++ b/package.json @@ -60,12 +60,14 @@ "build:devnet": "rimraf ./dist && yarn clear:webpack-cache && cross-env MIDEN_NETWORK=devnet DISABLE_TS_CHECKER=true NODE_ENV=development MODE_ENV=production MANIFEST_VERSION=3 webpack", "dev:devnet": "cross-env MIDEN_NETWORK=devnet DISABLE_TS_CHECKER=true NODE_ENV=development MODE_ENV=development MANIFEST_VERSION=3 webpack --watch --progress", "build:mobile:devnet": "rimraf ./dist/mobile && cross-env MIDEN_NETWORK=devnet DISABLE_TS_CHECKER=true NODE_ENV=development MODE_ENV=production webpack --config webpack.mobile.config.js", + "mobile:ios:devnet": "yarn build:mobile:devnet && npx cap sync ios && npx cap open ios", "build:desktop:devnet": "rimraf ./dist/desktop && cross-env MIDEN_NETWORK=devnet DISABLE_TS_CHECKER=true NODE_ENV=development MODE_ENV=production webpack --config webpack.desktop.config.js", "tauri": "tauri" }, "dependencies": { "@capacitor/android": "^8.0.1", "@capacitor/app": "^8.0.0", + "@capacitor/browser": "^8.0.3", "@capacitor/core": "^8.0.1", "@capacitor/filesystem": "^8.0.0", "@capacitor/haptics": "^8.0.0", diff --git a/public/_locales/de/messages.json b/public/_locales/de/messages.json index 5c6fb91f..24ced19d 100644 --- a/public/_locales/de/messages.json +++ b/public/_locales/de/messages.json @@ -1513,6 +1513,63 @@ "message": "Verschlüsselte Wallet-Datei", "englishSource": "Encrypted Wallet File" }, + "cloudBackup": { + "message": "Cloud-Backup", + "englishSource": "Cloud Backup" + }, + "importWithCloudBackup": { + "message": "Aus Cloud-Backup importieren", + "englishSource": "Import from Cloud Backup" + }, + "importWithCloudBackupDescription": { + "message": "Stellen Sie Ihre Wallet-Daten von Google Drive wieder her. Sie benötigen weiterhin Ihre seed phrase, um den Import abzuschließen.", + "englishSource": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import." + }, + "importFromCloudBackup": { + "message": "Aus der Cloud importieren", + "englishSource": "Import from Cloud" + }, + "cloudSignIn": { + "message": "Melden Sie sich mit Google an", + "englishSource": "Sign in with Google" + }, + "cloudSignedIn": { + "message": "Angemeldet als $email$ ", + "englishSource": "Signed in as $email$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "backupPassword": { + "message": "Backup-Passwort", + "englishSource": "Backup Password" + }, + "cloudRestoring": { + "message": "Backup wird wiederhergestellt...", + "englishSource": "Restoring backup..." + }, + "cloudProbing": { + "message": "Sicherung wird überprüft...", + "englishSource": "Checking backup..." + }, + "cloudNoBackupFound": { + "message": "Für dieses Konto wurde kein Backup gefunden.", + "englishSource": "No backup found on this account." + }, + "cloudPasskeyRestoreHint": { + "message": "Dieses Backup wurde mit einem Passkey verschlüsselt. Zum Wiederherstellen authentifizieren.", + "englishSource": "This backup was encrypted with a passkey. Authenticate to restore." + }, + "cloudRestoreWithPasskey": { + "message": "Mit Passkey wiederherstellen", + "englishSource": "Restore with Passkey" + }, + "signingIn": { + "message": "Anmelden...", + "englishSource": "Signing in..." + }, "encryptedWalletFileDescription": { "message": "Geben Sie Ihr Passwort ein, um auf Ihre verschlüsselte Wallet-Datei zuzugreifen.", "englishSource": "Enter your password to access your Encrypted Wallet File." diff --git a/public/_locales/en/en.json b/public/_locales/en/en.json index 73f9866e..9405f541 100644 --- a/public/_locales/en/en.json +++ b/public/_locales/en/en.json @@ -346,6 +346,19 @@ "importWithEncryptedWalletFileDescription": "Upload your encrypted wallet file to securely import your account. Your data remains private, with only hashed information stored on-chain", "twitter": "X (Twitter)", "encryptedWalletFile": "Encrypted Wallet File", + "cloudBackup": "Cloud Backup", + "importWithCloudBackup": "Import from Cloud Backup", + "importWithCloudBackupDescription": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import.", + "importFromCloudBackup": "Import from Cloud", + "cloudSignIn": "Sign in with Google", + "cloudSignedIn": "Signed in as $email$", + "backupPassword": "Backup Password", + "cloudRestoring": "Restoring backup...", + "cloudProbing": "Checking backup...", + "cloudNoBackupFound": "No backup found on this account.", + "cloudPasskeyRestoreHint": "This backup was encrypted with a passkey. Authenticate to restore.", + "cloudRestoreWithPasskey": "Restore with Passkey", + "signingIn": "Signing in...", "encryptedWalletFileDescription": "Enter your password to access your Encrypted Wallet File.", "encryptedWalletFileDescriptionHardware": "Unlock with your passcode to access your Encrypted Wallet File.", "encryptedWalletFileConfirmation": "I will not share my Encrypted Wallet File with anyone, including Miden Wallet.", diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index e41ed9c5..08f5afb6 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -1482,6 +1482,63 @@ "message": "Encrypted Wallet File", "englishSource": "Encrypted Wallet File" }, + "cloudBackup": { + "message": "Cloud Backup", + "englishSource": "Cloud Backup" + }, + "importWithCloudBackup": { + "message": "Import from Cloud Backup", + "englishSource": "Import from Cloud Backup" + }, + "importWithCloudBackupDescription": { + "message": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import.", + "englishSource": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import." + }, + "importFromCloudBackup": { + "message": "Import from Cloud", + "englishSource": "Import from Cloud" + }, + "cloudSignIn": { + "message": "Sign in with Google", + "englishSource": "Sign in with Google" + }, + "cloudSignedIn": { + "message": "Signed in as $email$", + "englishSource": "Signed in as $email$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "backupPassword": { + "message": "Backup Password", + "englishSource": "Backup Password" + }, + "cloudRestoring": { + "message": "Restoring backup...", + "englishSource": "Restoring backup..." + }, + "cloudProbing": { + "message": "Checking backup...", + "englishSource": "Checking backup..." + }, + "cloudNoBackupFound": { + "message": "No backup found on this account.", + "englishSource": "No backup found on this account." + }, + "cloudPasskeyRestoreHint": { + "message": "This backup was encrypted with a passkey. Authenticate to restore.", + "englishSource": "This backup was encrypted with a passkey. Authenticate to restore." + }, + "cloudRestoreWithPasskey": { + "message": "Restore with Passkey", + "englishSource": "Restore with Passkey" + }, + "signingIn": { + "message": "Signing in...", + "englishSource": "Signing in..." + }, "encryptedWalletFileDescription": { "message": "Enter your password to access your Encrypted Wallet File.", "englishSource": "Enter your password to access your Encrypted Wallet File." diff --git a/public/_locales/en_GB/messages.json b/public/_locales/en_GB/messages.json index bb19a3a1..f4360718 100644 --- a/public/_locales/en_GB/messages.json +++ b/public/_locales/en_GB/messages.json @@ -1541,6 +1541,63 @@ "message": "Encrypted Wallet File", "englishSource": "Encrypted Wallet File" }, + "cloudBackup": { + "message": "Cloud Backup", + "englishSource": "Cloud Backup" + }, + "importWithCloudBackup": { + "message": "Import from Cloud Backup", + "englishSource": "Import from Cloud Backup" + }, + "importWithCloudBackupDescription": { + "message": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import.", + "englishSource": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import." + }, + "importFromCloudBackup": { + "message": "Import from Cloud", + "englishSource": "Import from Cloud" + }, + "cloudSignIn": { + "message": "Sign in with Google", + "englishSource": "Sign in with Google" + }, + "cloudSignedIn": { + "message": "Signed in as $email$ ", + "englishSource": "Signed in as $email$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "backupPassword": { + "message": "Backup Password", + "englishSource": "Backup Password" + }, + "cloudRestoring": { + "message": "Restoring backup...", + "englishSource": "Restoring backup..." + }, + "cloudProbing": { + "message": "Checking backup...", + "englishSource": "Checking backup..." + }, + "cloudNoBackupFound": { + "message": "No backup found on this account.", + "englishSource": "No backup found on this account." + }, + "cloudPasskeyRestoreHint": { + "message": "This backup was encrypted with a passkey. Authenticate to restore.", + "englishSource": "This backup was encrypted with a passkey. Authenticate to restore." + }, + "cloudRestoreWithPasskey": { + "message": "Restore with Passkey", + "englishSource": "Restore with Passkey" + }, + "signingIn": { + "message": "Signing in...", + "englishSource": "Signing in..." + }, "encryptedWalletFileDescription": { "message": "Enter your password to access your Encrypted Wallet File.", "englishSource": "Enter your password to access your Encrypted Wallet File." diff --git a/public/_locales/es/messages.json b/public/_locales/es/messages.json index a9c1395e..ab98cd2a 100644 --- a/public/_locales/es/messages.json +++ b/public/_locales/es/messages.json @@ -1455,6 +1455,63 @@ "message": "Archivo de billetera cifrado", "englishSource": "Encrypted Wallet File" }, + "cloudBackup": { + "message": "Copia de seguridad en la nube", + "englishSource": "Cloud Backup" + }, + "importWithCloudBackup": { + "message": "Importar desde copia de seguridad en la nube", + "englishSource": "Import from Cloud Backup" + }, + "importWithCloudBackupDescription": { + "message": "Restaure los datos de su billetera desde Google Drive. Aún necesitarás tu seed phrase para completar la importación.", + "englishSource": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import." + }, + "importFromCloudBackup": { + "message": "Importar desde la nube", + "englishSource": "Import from Cloud" + }, + "cloudSignIn": { + "message": "Iniciar sesión con Google", + "englishSource": "Sign in with Google" + }, + "cloudSignedIn": { + "message": "Iniciado sesión como $email$ ", + "englishSource": "Signed in as $email$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "backupPassword": { + "message": "Contraseña de respaldo", + "englishSource": "Backup Password" + }, + "cloudRestoring": { + "message": "Restaurando copia de seguridad...", + "englishSource": "Restoring backup..." + }, + "cloudProbing": { + "message": "Comprobando copia de seguridad...", + "englishSource": "Checking backup..." + }, + "cloudNoBackupFound": { + "message": "No se encontró ninguna copia de seguridad en esta cuenta.", + "englishSource": "No backup found on this account." + }, + "cloudPasskeyRestoreHint": { + "message": "Esta copia de seguridad se cifró con una clave de acceso. Autenticar para restaurar.", + "englishSource": "This backup was encrypted with a passkey. Authenticate to restore." + }, + "cloudRestoreWithPasskey": { + "message": "Restaurar con clave de acceso", + "englishSource": "Restore with Passkey" + }, + "signingIn": { + "message": "Iniciando sesión...", + "englishSource": "Signing in..." + }, "encryptedWalletFileDescription": { "message": "Ingrese su contraseña para acceder a su archivo de billetera cifrado.", "englishSource": "Enter your password to access your Encrypted Wallet File." diff --git a/public/_locales/fr/messages.json b/public/_locales/fr/messages.json index d348655c..a5af5151 100644 --- a/public/_locales/fr/messages.json +++ b/public/_locales/fr/messages.json @@ -1512,6 +1512,63 @@ "message": "Fichier de portefeuille crypté", "englishSource": "Encrypted Wallet File" }, + "cloudBackup": { + "message": "Sauvegarde dans le cloud", + "englishSource": "Cloud Backup" + }, + "importWithCloudBackup": { + "message": "Importer depuis la sauvegarde cloud", + "englishSource": "Import from Cloud Backup" + }, + "importWithCloudBackupDescription": { + "message": "Restaurez les données de votre portefeuille depuis Google Drive. Vous aurez toujours besoin de votre seed phrase pour terminer l'importation.", + "englishSource": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import." + }, + "importFromCloudBackup": { + "message": "Importer depuis le cloud", + "englishSource": "Import from Cloud" + }, + "cloudSignIn": { + "message": "Connectez-vous avec Google", + "englishSource": "Sign in with Google" + }, + "cloudSignedIn": { + "message": "Connecté en tant que $email$ ", + "englishSource": "Signed in as $email$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "backupPassword": { + "message": "Mot de passe de sauvegarde", + "englishSource": "Backup Password" + }, + "cloudRestoring": { + "message": "Restauration de la sauvegarde...", + "englishSource": "Restoring backup..." + }, + "cloudProbing": { + "message": "Vérification de la sauvegarde...", + "englishSource": "Checking backup..." + }, + "cloudNoBackupFound": { + "message": "Aucune sauvegarde trouvée sur ce compte.", + "englishSource": "No backup found on this account." + }, + "cloudPasskeyRestoreHint": { + "message": "Cette sauvegarde a été chiffrée avec un mot de passe. Authenticate to restore.", + "englishSource": "This backup was encrypted with a passkey. Authenticate to restore." + }, + "cloudRestoreWithPasskey": { + "message": "Restaurer avec la clé d'accès", + "englishSource": "Restore with Passkey" + }, + "signingIn": { + "message": "Connexion...", + "englishSource": "Signing in..." + }, "encryptedWalletFileDescription": { "message": "Entrez votre mot de passe pour accéder à votre fichier de portefeuille crypté.", "englishSource": "Enter your password to access your Encrypted Wallet File." diff --git a/public/_locales/ja/messages.json b/public/_locales/ja/messages.json index 3be461fa..9bc86abd 100644 --- a/public/_locales/ja/messages.json +++ b/public/_locales/ja/messages.json @@ -1513,6 +1513,63 @@ "message": "暗号化されたウォレットファイル", "englishSource": "Encrypted Wallet File" }, + "cloudBackup": { + "message": "クラウドバックアップ", + "englishSource": "Cloud Backup" + }, + "importWithCloudBackup": { + "message": "クラウドバックアップからインポート", + "englishSource": "Import from Cloud Backup" + }, + "importWithCloudBackupDescription": { + "message": "Googleドライブからウォレットデータを復元します。インポートを完了するには、seed phrase が必要になります。", + "englishSource": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import." + }, + "importFromCloudBackup": { + "message": "クラウドからインポート", + "englishSource": "Import from Cloud" + }, + "cloudSignIn": { + "message": "Googleでサインイン", + "englishSource": "Sign in with Google" + }, + "cloudSignedIn": { + "message": "としてサインインしました $email$ ", + "englishSource": "Signed in as $email$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "backupPassword": { + "message": "バックアップパスワード", + "englishSource": "Backup Password" + }, + "cloudRestoring": { + "message": "バックアップを復元中...", + "englishSource": "Restoring backup..." + }, + "cloudProbing": { + "message": "バックアップを確認しています...", + "englishSource": "Checking backup..." + }, + "cloudNoBackupFound": { + "message": "このアカウントにはバックアップが見つかりませんでした。", + "englishSource": "No backup found on this account." + }, + "cloudPasskeyRestoreHint": { + "message": "このバックアップはパスキーで暗号化されていました。認証して復元します。", + "englishSource": "This backup was encrypted with a passkey. Authenticate to restore." + }, + "cloudRestoreWithPasskey": { + "message": "パスキーを使用して復元する", + "englishSource": "Restore with Passkey" + }, + "signingIn": { + "message": "サインイン中...", + "englishSource": "Signing in..." + }, "encryptedWalletFileDescription": { "message": "パスワードを入力して、暗号化されたウォレット ファイルにアクセスします。", "englishSource": "Enter your password to access your Encrypted Wallet File." diff --git a/public/_locales/ko/messages.json b/public/_locales/ko/messages.json index a1be9d68..26020a47 100644 --- a/public/_locales/ko/messages.json +++ b/public/_locales/ko/messages.json @@ -1513,6 +1513,63 @@ "message": "암호화된 지갑 파일", "englishSource": "Encrypted Wallet File" }, + "cloudBackup": { + "message": "클라우드 백업", + "englishSource": "Cloud Backup" + }, + "importWithCloudBackup": { + "message": "클라우드 백업에서 가져오기", + "englishSource": "Import from Cloud Backup" + }, + "importWithCloudBackupDescription": { + "message": "Google 드라이브에서 지갑 데이터를 복원하세요. 가져오기를 완료하려면 seed phrase이 필요합니다.", + "englishSource": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import." + }, + "importFromCloudBackup": { + "message": "클라우드에서 가져오기", + "englishSource": "Import from Cloud" + }, + "cloudSignIn": { + "message": "Google로 로그인", + "englishSource": "Sign in with Google" + }, + "cloudSignedIn": { + "message": "다음 계정으로 로그인됨 $email$ ", + "englishSource": "Signed in as $email$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "backupPassword": { + "message": "백업 비밀번호", + "englishSource": "Backup Password" + }, + "cloudRestoring": { + "message": "백업 복원 중...", + "englishSource": "Restoring backup..." + }, + "cloudProbing": { + "message": "백업 확인 중...", + "englishSource": "Checking backup..." + }, + "cloudNoBackupFound": { + "message": "이 계정에는 백업이 없습니다.", + "englishSource": "No backup found on this account." + }, + "cloudPasskeyRestoreHint": { + "message": "이 백업은 암호키로 암호화되었습니다. 복원하려면 인증하세요.", + "englishSource": "This backup was encrypted with a passkey. Authenticate to restore." + }, + "cloudRestoreWithPasskey": { + "message": "패스키로 복원", + "englishSource": "Restore with Passkey" + }, + "signingIn": { + "message": "로그인 중...", + "englishSource": "Signing in..." + }, "encryptedWalletFileDescription": { "message": "암호화된 지갑 파일에 접근하려면 비밀번호를 입력하세요.", "englishSource": "Enter your password to access your Encrypted Wallet File." diff --git a/public/_locales/pl/messages.json b/public/_locales/pl/messages.json index 8662e225..a4e47666 100644 --- a/public/_locales/pl/messages.json +++ b/public/_locales/pl/messages.json @@ -1455,6 +1455,63 @@ "message": "Zaszyfrowany plik portfela", "englishSource": "Encrypted Wallet File" }, + "cloudBackup": { + "message": "Kopia zapasowa w chmurze", + "englishSource": "Cloud Backup" + }, + "importWithCloudBackup": { + "message": "Importuj z kopii zapasowej w chmurze", + "englishSource": "Import from Cloud Backup" + }, + "importWithCloudBackupDescription": { + "message": "Przywróć dane portfela z Dysku Google. Do ukończenia importu nadal będziesz potrzebować seed phrase.", + "englishSource": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import." + }, + "importFromCloudBackup": { + "message": "Importuj z chmury", + "englishSource": "Import from Cloud" + }, + "cloudSignIn": { + "message": "Zaloguj się za pomocą Google", + "englishSource": "Sign in with Google" + }, + "cloudSignedIn": { + "message": "Zalogowałem się jako $email$ ", + "englishSource": "Signed in as $email$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "backupPassword": { + "message": "Hasło zapasowe", + "englishSource": "Backup Password" + }, + "cloudRestoring": { + "message": "Przywracam kopię zapasową...", + "englishSource": "Restoring backup..." + }, + "cloudProbing": { + "message": "Sprawdzam kopię zapasową...", + "englishSource": "Checking backup..." + }, + "cloudNoBackupFound": { + "message": "Na tym koncie nie znaleziono kopii zapasowej.", + "englishSource": "No backup found on this account." + }, + "cloudPasskeyRestoreHint": { + "message": "Ta kopia zapasowa została zaszyfrowana za pomocą klucza. Uwierzytelnij, aby przywrócić.", + "englishSource": "This backup was encrypted with a passkey. Authenticate to restore." + }, + "cloudRestoreWithPasskey": { + "message": "Przywróć za pomocą klucza dostępu", + "englishSource": "Restore with Passkey" + }, + "signingIn": { + "message": "Logowanie...", + "englishSource": "Signing in..." + }, "encryptedWalletFileDescription": { "message": "Wprowadź hasło, aby uzyskać dostęp do zaszyfrowanego pliku portfela.", "englishSource": "Enter your password to access your Encrypted Wallet File." diff --git a/public/_locales/pt/messages.json b/public/_locales/pt/messages.json index 533bfd1f..84761404 100644 --- a/public/_locales/pt/messages.json +++ b/public/_locales/pt/messages.json @@ -1511,6 +1511,63 @@ "message": "Arquivo de carteira criptografado", "englishSource": "Encrypted Wallet File" }, + "cloudBackup": { + "message": "Backup na nuvem", + "englishSource": "Cloud Backup" + }, + "importWithCloudBackup": { + "message": "Importar do backup na nuvem", + "englishSource": "Import from Cloud Backup" + }, + "importWithCloudBackupDescription": { + "message": "Restaure os dados da sua carteira do Google Drive. Você ainda precisará do seu seed phrase para concluir a importação.", + "englishSource": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import." + }, + "importFromCloudBackup": { + "message": "Importar da nuvem", + "englishSource": "Import from Cloud" + }, + "cloudSignIn": { + "message": "Faça login com o Google", + "englishSource": "Sign in with Google" + }, + "cloudSignedIn": { + "message": "Conectado como $email$ ", + "englishSource": "Signed in as $email$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "backupPassword": { + "message": "Senha de backup", + "englishSource": "Backup Password" + }, + "cloudRestoring": { + "message": "Restaurando backup...", + "englishSource": "Restoring backup..." + }, + "cloudProbing": { + "message": "Verificando backup...", + "englishSource": "Checking backup..." + }, + "cloudNoBackupFound": { + "message": "Nenhum backup encontrado nesta conta.", + "englishSource": "No backup found on this account." + }, + "cloudPasskeyRestoreHint": { + "message": "Este backup foi criptografado com uma chave de acesso. Autentique para restaurar.", + "englishSource": "This backup was encrypted with a passkey. Authenticate to restore." + }, + "cloudRestoreWithPasskey": { + "message": "Restaurar com senha", + "englishSource": "Restore with Passkey" + }, + "signingIn": { + "message": "Fazendo login...", + "englishSource": "Signing in..." + }, "encryptedWalletFileDescription": { "message": "Digite sua senha para acessar seu arquivo criptografado da carteira.", "englishSource": "Enter your password to access your Encrypted Wallet File." diff --git a/public/_locales/ru/messages.json b/public/_locales/ru/messages.json index 1a1d2c6a..45010262 100644 --- a/public/_locales/ru/messages.json +++ b/public/_locales/ru/messages.json @@ -1514,6 +1514,63 @@ "message": "Зашифрованный файл кошелька", "englishSource": "Encrypted Wallet File" }, + "cloudBackup": { + "message": "Облачное резервное копирование", + "englishSource": "Cloud Backup" + }, + "importWithCloudBackup": { + "message": "Импорт из облачного резервного копирования", + "englishSource": "Import from Cloud Backup" + }, + "importWithCloudBackupDescription": { + "message": "Восстановите данные своего кошелька с Google Диска. Для завершения импорта вам все равно понадобится ваш seed phrase.", + "englishSource": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import." + }, + "importFromCloudBackup": { + "message": "Импорт из облака", + "englishSource": "Import from Cloud" + }, + "cloudSignIn": { + "message": "Войти через Google", + "englishSource": "Sign in with Google" + }, + "cloudSignedIn": { + "message": "Вошёл как $email$ ", + "englishSource": "Signed in as $email$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "backupPassword": { + "message": "Резервный пароль", + "englishSource": "Backup Password" + }, + "cloudRestoring": { + "message": "Восстановление резервной копии...", + "englishSource": "Restoring backup..." + }, + "cloudProbing": { + "message": "Проверяем резервную копию...", + "englishSource": "Checking backup..." + }, + "cloudNoBackupFound": { + "message": "В этом аккаунте не найдено резервных копий.", + "englishSource": "No backup found on this account." + }, + "cloudPasskeyRestoreHint": { + "message": "Эта резервная копия была зашифрована с помощью пароля. Авторизуйтесь для восстановления.", + "englishSource": "This backup was encrypted with a passkey. Authenticate to restore." + }, + "cloudRestoreWithPasskey": { + "message": "Восстановить с помощью пароля", + "englishSource": "Restore with Passkey" + }, + "signingIn": { + "message": "Вход в систему...", + "englishSource": "Signing in..." + }, "encryptedWalletFileDescription": { "message": "Введите свой пароль для доступа к вашему зашифрованному файлу кошелька.", "englishSource": "Enter your password to access your Encrypted Wallet File." diff --git a/public/_locales/tr/messages.json b/public/_locales/tr/messages.json index 5ce25ab4..7bf86664 100644 --- a/public/_locales/tr/messages.json +++ b/public/_locales/tr/messages.json @@ -1513,6 +1513,63 @@ "message": "Şifreli Cüzdan Dosyası", "englishSource": "Encrypted Wallet File" }, + "cloudBackup": { + "message": "Bulut Yedekleme", + "englishSource": "Cloud Backup" + }, + "importWithCloudBackup": { + "message": "Bulut Yedekleme'den içe aktar", + "englishSource": "Import from Cloud Backup" + }, + "importWithCloudBackupDescription": { + "message": "Cüzdan verilerinizi Google Drive'dan geri yükleyin. İçe aktarma işlemini tamamlamak için yine de seed phrase numaranıza ihtiyacınız olacak.", + "englishSource": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import." + }, + "importFromCloudBackup": { + "message": "Buluttan İçe Aktar", + "englishSource": "Import from Cloud" + }, + "cloudSignIn": { + "message": "Google ile oturum açın", + "englishSource": "Sign in with Google" + }, + "cloudSignedIn": { + "message": "Şu şekilde oturum açıldı: $email$ ", + "englishSource": "Signed in as $email$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "backupPassword": { + "message": "Yedekleme Şifresi", + "englishSource": "Backup Password" + }, + "cloudRestoring": { + "message": "Yedekleme geri yükleniyor...", + "englishSource": "Restoring backup..." + }, + "cloudProbing": { + "message": "Yedekleme kontrol ediliyor...", + "englishSource": "Checking backup..." + }, + "cloudNoBackupFound": { + "message": "Bu hesapta yedek bulunamadı.", + "englishSource": "No backup found on this account." + }, + "cloudPasskeyRestoreHint": { + "message": "Bu yedekleme bir geçiş anahtarıyla şifrelendi. Geri yüklemek için kimlik doğrulaması yapın.", + "englishSource": "This backup was encrypted with a passkey. Authenticate to restore." + }, + "cloudRestoreWithPasskey": { + "message": "Şifre Anahtarı ile Geri Yükle", + "englishSource": "Restore with Passkey" + }, + "signingIn": { + "message": "Oturum açılıyor...", + "englishSource": "Signing in..." + }, "encryptedWalletFileDescription": { "message": "Şifreli Cüzdan Dosyanıza erişmek için şifrenizi girin.", "englishSource": "Enter your password to access your Encrypted Wallet File." diff --git a/public/_locales/uk/messages.json b/public/_locales/uk/messages.json index 415d5cf0..c81ae757 100644 --- a/public/_locales/uk/messages.json +++ b/public/_locales/uk/messages.json @@ -1514,6 +1514,63 @@ "message": "Зашифрований файл гаманця", "englishSource": "Encrypted Wallet File" }, + "cloudBackup": { + "message": "Хмарне резервне копіювання", + "englishSource": "Cloud Backup" + }, + "importWithCloudBackup": { + "message": "Імпорт із Cloud Backup", + "englishSource": "Import from Cloud Backup" + }, + "importWithCloudBackupDescription": { + "message": "Відновіть дані свого гаманця з Google Drive. Для завершення імпорту вам знадобиться ваш seed phrase.", + "englishSource": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import." + }, + "importFromCloudBackup": { + "message": "Імпорт із хмари", + "englishSource": "Import from Cloud" + }, + "cloudSignIn": { + "message": "Увійдіть за допомогою Google", + "englishSource": "Sign in with Google" + }, + "cloudSignedIn": { + "message": "Ви ввійшли як $email$ ", + "englishSource": "Signed in as $email$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "backupPassword": { + "message": "Резервний пароль", + "englishSource": "Backup Password" + }, + "cloudRestoring": { + "message": "Відновлення резервної копії...", + "englishSource": "Restoring backup..." + }, + "cloudProbing": { + "message": "Перевірка резервної копії...", + "englishSource": "Checking backup..." + }, + "cloudNoBackupFound": { + "message": "Для цього облікового запису не знайдено резервної копії.", + "englishSource": "No backup found on this account." + }, + "cloudPasskeyRestoreHint": { + "message": "Ця резервна копія була зашифрована за допомогою ключа доступу. Щоб відновити, пройдіть автентифікацію.", + "englishSource": "This backup was encrypted with a passkey. Authenticate to restore." + }, + "cloudRestoreWithPasskey": { + "message": "Відновити за допомогою пароля", + "englishSource": "Restore with Passkey" + }, + "signingIn": { + "message": "Вхід...", + "englishSource": "Signing in..." + }, "encryptedWalletFileDescription": { "message": "Введіть свій пароль, щоб отримати доступ до зашифрованого файлу гаманця.", "englishSource": "Enter your password to access your Encrypted Wallet File." diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json index 46e6fa83..6807c9ec 100644 --- a/public/_locales/zh_CN/messages.json +++ b/public/_locales/zh_CN/messages.json @@ -1513,6 +1513,63 @@ "message": "加密钱包文件", "englishSource": "Encrypted Wallet File" }, + "cloudBackup": { + "message": "云备份", + "englishSource": "Cloud Backup" + }, + "importWithCloudBackup": { + "message": "从云备份导入", + "englishSource": "Import from Cloud Backup" + }, + "importWithCloudBackupDescription": { + "message": "从 Google 云端硬盘恢复您的钱包数据。您仍然需要 seed phrase 来完成导入。", + "englishSource": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import." + }, + "importFromCloudBackup": { + "message": "从云端导入", + "englishSource": "Import from Cloud" + }, + "cloudSignIn": { + "message": "使用 Google 登录", + "englishSource": "Sign in with Google" + }, + "cloudSignedIn": { + "message": "登录身份 $email$ ", + "englishSource": "Signed in as $email$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "backupPassword": { + "message": "备份密码", + "englishSource": "Backup Password" + }, + "cloudRestoring": { + "message": "正在恢复备份...", + "englishSource": "Restoring backup..." + }, + "cloudProbing": { + "message": "正在检查备份...", + "englishSource": "Checking backup..." + }, + "cloudNoBackupFound": { + "message": "在此帐户上找不到备份。", + "englishSource": "No backup found on this account." + }, + "cloudPasskeyRestoreHint": { + "message": "该备份已使用密钥加密。进行身份验证即可恢复。", + "englishSource": "This backup was encrypted with a passkey. Authenticate to restore." + }, + "cloudRestoreWithPasskey": { + "message": "使用密钥恢复", + "englishSource": "Restore with Passkey" + }, + "signingIn": { + "message": "正在登录...", + "englishSource": "Signing in..." + }, "encryptedWalletFileDescription": { "message": "输入您的密码以访问您的加密钱包文件。", "englishSource": "Enter your password to access your Encrypted Wallet File." diff --git a/public/_locales/zh_TW/messages.json b/public/_locales/zh_TW/messages.json index 05fcaa59..80b87fb0 100644 --- a/public/_locales/zh_TW/messages.json +++ b/public/_locales/zh_TW/messages.json @@ -1513,6 +1513,63 @@ "message": "加密钱包文件", "englishSource": "Encrypted Wallet File" }, + "cloudBackup": { + "message": "云备份", + "englishSource": "Cloud Backup" + }, + "importWithCloudBackup": { + "message": "从云备份导入", + "englishSource": "Import from Cloud Backup" + }, + "importWithCloudBackupDescription": { + "message": "从 Google 云端硬盘恢复您的钱包数据。您仍然需要 seed phrase 来完成导入。", + "englishSource": "Restore your wallet data from Google Drive. You will still need your seed phrase to complete the import." + }, + "importFromCloudBackup": { + "message": "从云端导入", + "englishSource": "Import from Cloud" + }, + "cloudSignIn": { + "message": "使用 Google 登录", + "englishSource": "Sign in with Google" + }, + "cloudSignedIn": { + "message": "登录身份 $email$ ", + "englishSource": "Signed in as $email$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "backupPassword": { + "message": "备份密码", + "englishSource": "Backup Password" + }, + "cloudRestoring": { + "message": "正在恢复备份...", + "englishSource": "Restoring backup..." + }, + "cloudProbing": { + "message": "正在检查备份...", + "englishSource": "Checking backup..." + }, + "cloudNoBackupFound": { + "message": "在此帐户上找不到备份。", + "englishSource": "No backup found on this account." + }, + "cloudPasskeyRestoreHint": { + "message": "该备份已使用密钥加密。进行身份验证即可恢复。", + "englishSource": "This backup was encrypted with a passkey. Authenticate to restore." + }, + "cloudRestoreWithPasskey": { + "message": "使用密钥恢复", + "englishSource": "Restore with Passkey" + }, + "signingIn": { + "message": "正在登录...", + "englishSource": "Signing in..." + }, "encryptedWalletFileDescription": { "message": "输入您的密码以访问您的加密钱包文件。", "englishSource": "Enter your password to access your Encrypted Wallet File." diff --git a/public/manifest.json b/public/manifest.json index 0c91ec11..b3acbeb7 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -13,11 +13,13 @@ "__chrome|firefox|opera__homepage_url": "https://miden.fi", "short_name": "Miden Wallet", - "permissions": ["storage", "clipboardWrite", "unlimitedStorage", "activeTab", "tabs", "notifications", "alarms", "sidePanel"], - "host_permissions": [ - "http://localhost/*", - "https://*.miden.fi/*" - ], + "permissions": ["storage", "clipboardWrite", "unlimitedStorage", "activeTab", "tabs", "notifications", "alarms", "sidePanel", "identity"], + "host_permissions": ["http://localhost/*", "https://*.miden.fi/*", "https://accounts.google.com/*", "https://www.googleapis.com/*"], + + "oauth2": { + "client_id": "849882985138-egh1ol1n5clvlvb2th2f6njdrg7v8de9.apps.googleusercontent.com", + "scopes": ["https://www.googleapis.com/auth/drive.appdata"] + }, "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" diff --git a/public/manifest.v2.json b/public/manifest.v2.json index ced20a94..fc405d04 100644 --- a/public/manifest.v2.json +++ b/public/manifest.v2.json @@ -19,10 +19,19 @@ "unlimitedStorage", "clipboardWrite", "activeTab", + "identity", "http://localhost:4180/", "http://localhost:3000/", - "https://*.miden.fi/*" + "https://*.miden.fi/*", + "https://accounts.google.com/*", + "https://www.googleapis.com/*" ], + + "oauth2": { + "client_id": "849882985138-egh1ol1n5clvlvb2th2f6njdrg7v8de9.apps.googleusercontent.com", + "scopes": ["https://www.googleapis.com/auth/drive.appdata"] + }, + "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", "__chrome|firefox__author": "Miden", diff --git a/src/app/pages/ForgotPassword/ForgotPassword.tsx b/src/app/pages/ForgotPassword/ForgotPassword.tsx index c434abbc..1f013451 100644 --- a/src/app/pages/ForgotPassword/ForgotPassword.tsx +++ b/src/app/pages/ForgotPassword/ForgotPassword.tsx @@ -4,35 +4,54 @@ import { generateMnemonic } from 'bip39'; import wordsList from 'bip39/src/wordlists/english.json'; import { formatMnemonic } from 'app/defaults'; +import { persistGoogleRefreshToken } from 'lib/miden/backup/google-drive-auth'; import { useMidenContext } from 'lib/miden/front'; import { clearClientStorage } from 'lib/miden/reset'; import { useMobileBackHandler } from 'lib/mobile/useMobileBackHandler'; +import { CloudBackupCredentials } from 'lib/shared/types'; import { navigate } from 'lib/woozie'; import { ForgotPasswordFlow } from 'screens/onboarding/forgot-password-navigator'; -import { ForgotPasswordAction, ForgotPasswordStep, OnboardingType } from 'screens/onboarding/types'; +import { ForgotPasswordAction, ForgotPasswordStep, ImportType, OnboardingType } from 'screens/onboarding/types'; const ForgotPassword: FC = () => { const [step, setStep] = useState(ForgotPasswordStep.Welcome); const [seedPhrase, setSeedPhrase] = useState([]); const [onboardingType, setOnboardingType] = useState(null); const [password, setPassword] = useState(null); + const [importType, setImportType] = useState(null); const [importedWithFile, setImportedWithFile] = useState(false); const [isLoading, setIsLoading] = useState(false); - const { registerWallet, importWalletFromClient } = useMidenContext(); + const { registerWallet, importWalletFromClient, registerFromCloudBackup, setAutoBackupEnabled } = useMidenContext(); + const [cloudBackupData, setCloudBackupData] = useState(null); const register = useCallback(async () => { if (password && seedPhrase) { clearClientStorage(); const seedPhraseFormatted = formatMnemonic(seedPhrase.join(' ')); - if (!importedWithFile) { + if (cloudBackupData) { try { - await registerWallet( + await registerFromCloudBackup( password, seedPhraseFormatted, - onboardingType === OnboardingType.Import // might be able to leverage ownMnemonic to determine whther to attempt imports in general + cloudBackupData.walletAccounts, + cloudBackupData.walletSettings ); + await persistGoogleRefreshToken(cloudBackupData.refreshToken); + await setAutoBackupEnabled( + true, + cloudBackupData.accessToken, + cloudBackupData.expiresAt, + cloudBackupData.encryption, + true + ); + } catch (e) { + console.error(e); + } + } else if (!importedWithFile) { + try { + await registerWallet(password, seedPhraseFormatted, onboardingType === OnboardingType.Import); } catch (e) { console.error(e); } @@ -44,7 +63,17 @@ const ForgotPassword: FC = () => { } } } - }, [password, seedPhrase, importedWithFile, registerWallet, onboardingType, importWalletFromClient]); + }, [ + password, + seedPhrase, + importedWithFile, + cloudBackupData, + registerWallet, + registerFromCloudBackup, + setAutoBackupEnabled, + onboardingType, + importWalletFromClient + ]); const onAction = useCallback( async (action: ForgotPasswordAction) => { @@ -61,6 +90,14 @@ const ForgotPassword: FC = () => { case 'import-from-file': setStep(ForgotPasswordStep.ImportFromFile); break; + case 'import-from-cloud': + setImportType(ImportType.CloudBackup); + setStep(ForgotPasswordStep.ImportFromCloud); + break; + case 'import-from-cloud-submit': + setCloudBackupData(action.payload); + setStep(ForgotPasswordStep.ImportFromSeed); + break; case 'import-wallet-file-submit': const seedPhrase = action.payload.split(' '); setSeedPhrase(seedPhrase); @@ -107,8 +144,14 @@ const ForgotPassword: FC = () => { } else { setStep(ForgotPasswordStep.ImportFromSeed); } - } else if (step === ForgotPasswordStep.ImportFromFile || step === ForgotPasswordStep.ImportFromSeed) { + } else if (step === ForgotPasswordStep.ImportFromCloud) { setStep(ForgotPasswordStep.SelectImportType); + } else if (step === ForgotPasswordStep.ImportFromFile || step === ForgotPasswordStep.ImportFromSeed) { + if (importType === ImportType.CloudBackup) { + setStep(ForgotPasswordStep.ImportFromCloud); + } else { + setStep(ForgotPasswordStep.SelectImportType); + } } break; default: diff --git a/src/app/pages/Settings.tsx b/src/app/pages/Settings.tsx index be2cb782..0004c5dc 100644 --- a/src/app/pages/Settings.tsx +++ b/src/app/pages/Settings.tsx @@ -42,6 +42,7 @@ import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from 'lib/ui/drawer' import { goBack, navigate } from 'lib/woozie'; import { EncryptedFileFlow } from 'screens/encrypted-file-flow/EncryptedFileManager'; +import CloudBackupSettings from '../templates/CloudBackupSettings'; import pkg from '../../../package.json'; import AdvancedSettings from './AdvancedSettings'; import NetworksSettings from './Networks'; @@ -147,6 +148,13 @@ const TAB_GROUPS: TabGroup[] = [ Component: EncryptedFileFlow, testID: SettingsSelectors.EncryptedWalletFile, hasOwnLayout: true + }, + { + slug: 'cloud-backup', + titleI18nKey: 'cloudBackup', + Icon: EncryptedWalletIcon, + Component: CloudBackupSettings, + isDrawer: true } ] }, diff --git a/src/app/pages/Welcome.tsx b/src/app/pages/Welcome.tsx index a562f5de..405e8918 100644 --- a/src/app/pages/Welcome.tsx +++ b/src/app/pages/Welcome.tsx @@ -5,10 +5,11 @@ import wordslist from 'bip39/src/wordlists/english.json'; import { formatMnemonic } from 'app/defaults'; import { AnalyticsEventCategory, useAnalytics } from 'lib/analytics'; +import { persistGoogleRefreshToken } from 'lib/miden/backup/google-drive-auth'; import { useMidenContext } from 'lib/miden/front'; import { useMobileBackHandler } from 'lib/mobile/useMobileBackHandler'; import { isDesktop, isMobile } from 'lib/platform'; -import { WalletStatus } from 'lib/shared/types'; +import { CloudBackupCredentials, WalletStatus } from 'lib/shared/types'; import { useWalletStore } from 'lib/store'; import { fetchStateFromBackend } from 'lib/store/hooks/useIntercomSync'; import { navigate, useLocation } from 'lib/woozie'; @@ -77,7 +78,8 @@ const Welcome: FC = () => { const [isHardwareSecurityAvailable, setIsHardwareSecurityAvailable] = useState(false); const [biometricAttempts, setBiometricAttempts] = useState(0); const [biometricError, setBiometricError] = useState(null); - const { registerWallet, importWalletFromClient } = useMidenContext(); + const { registerWallet, importWalletFromClient, registerFromCloudBackup, setAutoBackupEnabled } = useMidenContext(); + const [cloudBackupData, setCloudBackupData] = useState(null); const { trackEvent } = useAnalytics(); const syncFromBackend = useWalletStore(s => s.syncFromBackend); @@ -93,7 +95,23 @@ const Welcome: FC = () => { const seedPhraseFormatted = formatMnemonic(seedPhrase.join(' ')); // For hardware-only wallets, pass undefined as password const actualPassword = password === '__HARDWARE_ONLY__' ? undefined : password; - if (!importedWithFile) { + if (cloudBackupData) { + await registerFromCloudBackup( + actualPassword, + seedPhraseFormatted, + cloudBackupData.walletAccounts, + cloudBackupData.walletSettings + ); + // clearStorage wiped the refresh token — re-persist it, then enable auto-backup + await persistGoogleRefreshToken(cloudBackupData.refreshToken); + await setAutoBackupEnabled( + true, + cloudBackupData.accessToken, + cloudBackupData.expiresAt, + cloudBackupData.encryption, + true + ); + } else if (!importedWithFile) { await registerWallet(actualPassword, seedPhraseFormatted, onboardingType === OnboardingType.Import); } else { await importWalletFromClient(actualPassword, seedPhraseFormatted); @@ -101,7 +119,17 @@ const Welcome: FC = () => { } else { throw new Error('Missing password or seed phrase'); } - }, [password, seedPhrase, importedWithFile, registerWallet, onboardingType, importWalletFromClient]); + }, [ + password, + seedPhrase, + importedWithFile, + cloudBackupData, + registerWallet, + registerFromCloudBackup, + setAutoBackupEnabled, + onboardingType, + importWalletFromClient + ]); const onAction = async (action: OnboardingAction) => { let eventCategory = AnalyticsEventCategory.ButtonPress; @@ -137,6 +165,14 @@ const Welcome: FC = () => { } } break; + case 'import-from-cloud': + setImportType(ImportType.CloudBackup); + navigate('/#import-from-cloud'); + break; + case 'import-from-cloud-submit': + setCloudBackupData(action.payload); + navigate('/#import-from-seed'); + break; case 'import-from-seed': setImportType(ImportType.SeedPhrase); navigate('/#import-from-seed'); @@ -231,8 +267,14 @@ const Welcome: FC = () => { navigate('/#import-from-seed'); } } - } else if (step === OnboardingStep.ImportFromFile || step === OnboardingStep.ImportFromSeed) { + } else if (step === OnboardingStep.ImportFromCloud) { navigate('/#select-import-type'); + } else if (step === OnboardingStep.ImportFromFile || step === OnboardingStep.ImportFromSeed) { + if (importType === ImportType.CloudBackup) { + navigate('/#import-from-cloud'); + } else { + navigate('/#select-import-type'); + } } break; default: @@ -261,6 +303,9 @@ const Welcome: FC = () => { case '#import-from-file': setStep(OnboardingStep.ImportFromFile); break; + case '#import-from-cloud': + setStep(OnboardingStep.ImportFromCloud); + break; case '#backup-seed-phrase': setOnboardingType(OnboardingType.Create); setStep(OnboardingStep.BackupSeedPhrase); diff --git a/src/app/templates/CloudBackupSettings.tsx b/src/app/templates/CloudBackupSettings.tsx new file mode 100644 index 00000000..ccf59c69 --- /dev/null +++ b/src/app/templates/CloudBackupSettings.tsx @@ -0,0 +1,214 @@ +import React, { FC, useCallback, useEffect, useState } from 'react'; + +import { Button, ButtonVariant } from 'components/Button'; +import { getGoogleAuthToken, GoogleAuthResult, trySilentGoogleAuth } from 'lib/miden/backup/google-drive-auth'; +import { useMidenContext } from 'lib/miden/front'; +import { generateSalt } from 'lib/miden/passworder'; +import { getPasskeyProvider } from 'lib/passkey'; +import { u8ToB64 } from 'lib/shared/helpers'; +import { AutoBackupStatus } from 'lib/shared/types'; + +type EncryptionMethod = 'password' | 'passkey'; + +const CloudBackupSettings: FC<{ onClose?: () => void }> = () => { + const { setAutoBackupEnabled, fetchAutoBackupStatus } = useMidenContext(); + + const [auth, setAuth] = useState(null); + const [backupPassword, setBackupPassword] = useState(''); + const [encryptionMethod, setEncryptionMethod] = useState('password'); + const [status, setStatus] = useState(''); + const [loading, setLoading] = useState(false); + const [autoBackupStatus, setAutoBackupStatus] = useState(null); + + // Fetch auto-backup status and try silent Google auth on mount + useEffect(() => { + fetchAutoBackupStatus() + .then(setAutoBackupStatus) + .catch(() => {}); + trySilentGoogleAuth() + .then(result => { + if (result) setAuth(result); + }) + .catch(() => {}); + }, [fetchAutoBackupStatus]); + + const handleSignIn = useCallback(async () => { + setLoading(true); + setStatus('Signing in...'); + try { + const result = await getGoogleAuthToken(); + setAuth(result); + setStatus('Signed in with Google'); + } catch (err: unknown) { + setStatus(`Sign-in failed: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setLoading(false); + } + }, []); + + const handleEnable = useCallback(async () => { + if (!auth) return; + setLoading(true); + setStatus('Enabling auto-backup...'); + try { + if (encryptionMethod === 'passkey') { + const provider = await getPasskeyProvider(); + if (!provider) throw new Error('Passkeys not available on this platform'); + + const salt = generateSalt(); + const { keyMaterial, credentialId, prfSalt } = await provider.register(salt); + + await setAutoBackupEnabled(true, auth.accessToken, auth.expiresAt, { + method: 'passkey', + keyMaterial: u8ToB64(keyMaterial), + credentialId: u8ToB64(credentialId), + prfSalt: u8ToB64(prfSalt) + }); + } else { + if (!backupPassword) return; + await setAutoBackupEnabled(true, auth.accessToken, auth.expiresAt, { + method: 'password', + backupPassword + }); + } + setBackupPassword(''); + setStatus('Auto-backup enabled'); + const updated = await fetchAutoBackupStatus(); + setAutoBackupStatus(updated); + } catch (err: unknown) { + setStatus(`Failed: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setLoading(false); + } + }, [auth, backupPassword, encryptionMethod, setAutoBackupEnabled, fetchAutoBackupStatus]); + + const handleDisable = useCallback(async () => { + setLoading(true); + setStatus('Disabling auto-backup...'); + try { + await setAutoBackupEnabled(false); + setStatus('Auto-backup disabled'); + const updated = await fetchAutoBackupStatus(); + setAutoBackupStatus(updated); + } catch (err: unknown) { + setStatus(`Failed: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setLoading(false); + } + }, [setAutoBackupEnabled, fetchAutoBackupStatus]); + + const handleGoogleReauth = useCallback(async () => { + setLoading(true); + setStatus('Re-authenticating with Google...'); + try { + const result = await getGoogleAuthToken(); + setAuth(result); + // Re-enable with fresh token (encryption unchanged, just refreshing token) + setStatus('Re-authenticated with Google'); + } catch (err: unknown) { + setStatus(`Re-auth failed: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setLoading(false); + } + }, []); + + const isEnabled = autoBackupStatus?.enabled ?? false; + const enableDisabled = loading || !auth || (encryptionMethod === 'password' && !backupPassword); + + return ( +
+

Cloud Backup

+ + {/* Status */} + {isEnabled && autoBackupStatus && ( +
+

Auto-backup enabled ({autoBackupStatus.method})

+ {autoBackupStatus.lastBackupAt && ( +

Last backup: {new Date(autoBackupStatus.lastBackupAt).toLocaleString()}

+ )} + {autoBackupStatus.lastError &&

Error: {autoBackupStatus.lastError}

} + {autoBackupStatus.needsGoogleReauth && ( +
+ )} + + {/* Auth */} + {!isEnabled && ( + <> + + +
+ + {/* Password input (only for password method) */} + {encryptionMethod === 'password' && ( + setBackupPassword(e.target.value)} + className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm" + /> + )} + + {/* Enable */} + + {signInError &&

{signInError}

} + + ) : probing ? ( +

{t('cloudProbing')}

+ ) : noBackup ? ( +

{t('cloudNoBackupFound')}

+ ) : isPasskeyBackup ? ( +
+
{t('cloudSignedIn', { email: 'Google' })}
+

{t('cloudPasskeyRestoreHint')}

+ {restoreError &&

{restoreError}

} +
+ +
+
+ ) : isPasswordBackup ? ( +
+
{t('cloudSignedIn', { email: 'Google' })}
+ +
+ + {isSubmitting ? t('cloudRestoring') : t('import')} + +
+ + ) : null} + + ); +}; diff --git a/src/screens/onboarding/import-wallet-flow/SelectImportType.tsx b/src/screens/onboarding/import-wallet-flow/SelectImportType.tsx index 3f2d3c1b..f4ee21a7 100644 --- a/src/screens/onboarding/import-wallet-flow/SelectImportType.tsx +++ b/src/screens/onboarding/import-wallet-flow/SelectImportType.tsx @@ -30,7 +30,12 @@ export const SelectImportTypeScreen = ({ onSubmit }: SelectImportTypeScreenProps { id: ImportType.WalletFile, title: t('importWithEncryptedWalletFile'), - description: t('importWithEncryptedWalletFileDescription'), + description: t('importWithEncryptedWalletFileDescription') + }, + { + id: ImportType.CloudBackup, + title: t('importWithCloudBackup'), + description: t('importWithCloudBackupDescription'), isLast: true } ], diff --git a/src/screens/onboarding/navigator.tsx b/src/screens/onboarding/navigator.tsx index 0ca6dcf5..f6190748 100644 --- a/src/screens/onboarding/navigator.tsx +++ b/src/screens/onboarding/navigator.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'; import { Button, ButtonVariant } from 'components/Button'; import { ProgressIndicator } from 'components/ProgressIndicator'; import { isMobile } from 'lib/platform'; +import { CloudBackupCredentials } from 'lib/shared/types'; import { ConfirmationScreen } from './common/Confirmation'; import { CreatePasswordScreen } from './common/CreatePassword'; @@ -14,6 +15,7 @@ import { WelcomeScreen } from './common/Welcome'; import { BackUpSeedPhraseScreen } from './create-wallet-flow/BackUpSeedPhrase'; import { SelectTransactionTypeScreen } from './create-wallet-flow/SelectTransactionType'; import { VerifySeedPhraseScreen } from './create-wallet-flow/VerifySeedPhrase'; +import { ImportFromCloudScreen } from './import-wallet-flow/ImportFromCloud'; import { ImportSeedPhraseScreen } from './import-wallet-flow/ImportSeedPhrase'; import { ImportWalletFileScreen } from './import-wallet-flow/ImportWalletFile'; import { SelectImportTypeScreen } from './import-wallet-flow/SelectImportType'; @@ -49,7 +51,11 @@ const Header: React.FC<{ currentStep = 1; } else if (step === OnboardingStep.CreatePassword) { currentStep = 3; - } else if (step === OnboardingStep.ImportFromSeed || step === OnboardingStep.ImportFromFile) { + } else if ( + step === OnboardingStep.ImportFromSeed || + step === OnboardingStep.ImportFromFile || + step === OnboardingStep.ImportFromCloud + ) { currentStep = 2; } else if (step === OnboardingStep.Confirmation) { currentStep = 4; @@ -118,6 +124,11 @@ export const OnboardingFlow: FC = ({ id: 'import-from-file' }); break; + case ImportType.CloudBackup: + onForwardAction?.({ + id: 'import-from-cloud' + }); + break; default: break; } @@ -151,6 +162,10 @@ export const OnboardingFlow: FC = ({ onForwardAction?.({ id: 'import-wallet-file-submit', payload: seedPhrase }); }; + const onImportFromCloudSubmit = (payload: CloudBackupCredentials) => { + onForwardAction?.({ id: 'import-from-cloud-submit', payload }); + }; + switch (step) { case OnboardingStep.Welcome: return ; @@ -172,6 +187,8 @@ export const OnboardingFlow: FC = ({ return ; case OnboardingStep.ImportFromFile: return ; + case OnboardingStep.ImportFromCloud: + return ; case OnboardingStep.CreatePassword: return ; case OnboardingStep.SelectTransactionType: diff --git a/src/screens/onboarding/types.ts b/src/screens/onboarding/types.ts index cf94367a..99264281 100644 --- a/src/screens/onboarding/types.ts +++ b/src/screens/onboarding/types.ts @@ -1,3 +1,5 @@ +import { CloudBackupCredentials } from 'lib/shared/types'; + export enum OnboardingType { Create = 'create', Import = 'import' @@ -10,7 +12,8 @@ export enum WalletType { export enum ImportType { SeedPhrase = 'seed-phrase', - WalletFile = 'wallet-file' + WalletFile = 'wallet-file', + CloudBackup = 'cloud-backup' } export enum OnboardingStep { @@ -21,6 +24,7 @@ export enum OnboardingStep { SelectImportType = 'select-import-type', ImportFromSeed = 'import-from-seed', ImportFromFile = 'import-from-file', + ImportFromCloud = 'import-from-cloud', CreatePassword = 'create-password', BiometricSetup = 'biometric-setup', SelectTransactionType = 'select-transaction-type', @@ -39,7 +43,9 @@ export type OnboardingActionId = | 'select-transaction-type' | 'confirmation' | 'import-from-file' - | 'import-from-seed'; + | 'import-from-seed' + | 'import-from-cloud' + | 'import-from-cloud-submit'; export type CreateWalletAction = { id: 'create-wallet'; @@ -57,6 +63,15 @@ export type ImportFromSeedAction = { id: 'import-from-seed'; }; +export type ImportFromCloudAction = { + id: 'import-from-cloud'; +}; + +export type ImportFromCloudSubmitAction = { + id: 'import-from-cloud-submit'; + payload: CloudBackupCredentials; +}; + export type BackupSeedPhraseAction = { id: 'backup-seed-phrase'; }; @@ -121,6 +136,8 @@ export type OnboardingAction = | BackAction | ImportFromFileAction | ImportFromSeedAction + | ImportFromCloudAction + | ImportFromCloudSubmitAction | ImportWalletFileSubmitAction | SwitchToPasswordAction; @@ -137,6 +154,7 @@ export enum ForgotPasswordStep { SelectImportType = 'select-import-type', ImportFromSeed = 'import-from-seed', ImportFromFile = 'import-from-file', + ImportFromCloud = 'import-from-cloud', CreatePassword = 'create-password', SelectTransactionType = 'select-transaction-type', Confirmation = 'confirmation' @@ -150,6 +168,8 @@ export type ForgotPasswordAction = | SelectImportTypeAction | ImportFromSeedAction | ImportFromFileAction + | ImportFromCloudAction + | ImportFromCloudSubmitAction | ImportSeedPhraseSubmitAction | ImportWalletFileSubmitAction | CreatePasswordAction diff --git a/webpack.mobile.config.js b/webpack.mobile.config.js index fac8d98b..155d81fc 100644 --- a/webpack.mobile.config.js +++ b/webpack.mobile.config.js @@ -86,10 +86,7 @@ const mobileAppConfig = { } }, optimization: { - minimizer: [ - `...`, - new CssMinimizerPlugin() - ] + minimizer: [`...`, new CssMinimizerPlugin()] }, plugins: [ new Dotenv(), diff --git a/yarn.lock b/yarn.lock index fb2ee46b..b1bebc8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1018,6 +1018,11 @@ resolved "https://registry.yarnpkg.com/@capacitor/app/-/app-8.0.0.tgz#47c2a90302919d71cef8a7297fb1d6ee439d1250" integrity sha512-OwzIkUs4w433Bu9WWAEbEYngXEfJXZ9Wmdb8eoaqzYBgB0W9/3Ed/mh6sAYPNBAZlpyarmewgP7Nb+d3Vrh+xA== +"@capacitor/browser@^8.0.3": + version "8.0.3" + resolved "https://registry.yarnpkg.com/@capacitor/browser/-/browser-8.0.3.tgz#21c3543981c67196c08a74dc622eda96e548700b" + integrity sha512-WJWPHEPbweiFoHYmVlCbZf5yrqJ2Rchx2Xvbmd+3Lf+Zkpq3nXBThThY2CF69lYEg1NINGF9BcHThIOEU1gZlQ== + "@capacitor/cli@^8.0.1": version "8.0.1" resolved "https://registry.yarnpkg.com/@capacitor/cli/-/cli-8.0.1.tgz#34acb86b2920fa6d2ab1abb79e66c061863413fd" @@ -1102,6 +1107,11 @@ resolved "https://registry.yarnpkg.com/@capacitor/synapse/-/synapse-1.0.4.tgz#c6beb33119d9656b1f04cb7783989fb78933ef6d" integrity sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw== +"@capgo/inappbrowser@^8.0.6": + version "8.0.6" + resolved "https://registry.yarnpkg.com/@capgo/inappbrowser/-/inappbrowser-8.0.6.tgz#b9d5f241748b56634c0af9e88006186990ed521f" + integrity sha512-OTVM9p+zvhtPbjUjMJQNx3gLRigTLUBqvfQqYy4yOKFCRJvTVd/jdYbr9Xhe9GDO9JJzbCsVOPOAQloUvEcsUA== + "@chromatic-com/storybook@^1.5.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@chromatic-com/storybook/-/storybook-1.9.0.tgz#d95eb3474783bcc17a830a7627c3f099c1f75ba5" @@ -2159,17 +2169,13 @@ dexie "^4.0.1" glob "^11.0.0" -"@miden-sdk/react@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@miden-sdk/react/-/react-0.14.0.tgz#efedd9100c4aa33b00e5222c90e9c73e6691ee09" - integrity sha512-idTt+mZQyWXvN2eaLJWzGjqoqj9J/s3j1GSxFs8Y8sLeP+1HYfVw6nB6TGfZ9X3m4e27ZBAMtVO2S8uUeC5LqQ== +"@miden-sdk/react@0.14.0-alpha": + version "0.14.0-alpha" + resolved "https://registry.yarnpkg.com/@miden-sdk/react/-/react-0.14.0-alpha.tgz#80c9d22db7c417a07f98a8cecbf53f488fcc74d2" + integrity sha512-mDglifz4P2SaH8kA0i1j28o0t6KFLqh6yvLR3dicno+WLqyMASovLCLWSl4zMxXC4bI2s6xZIFnIXRjq/IaJMg== dependencies: zustand "^5.0.0" -"@miden/dapp-browser@link:./packages/dapp-browser": - version "0.0.0" - uid "" - "@napi-rs/wasm-runtime@^0.2.11": version "0.2.12" resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" @@ -2426,23 +2432,6 @@ "@resvg/resvg-js-win32-ia32-msvc" "2.6.2" "@resvg/resvg-js-win32-x64-msvc" "2.6.2" -"@rollup/plugin-typescript@^12.3.0": - version "12.3.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz#cc51b830973bc14c9456fe6532f322f2a40f5f12" - integrity sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big== - dependencies: - "@rollup/pluginutils" "^5.1.0" - resolve "^1.22.1" - -"@rollup/pluginutils@^5.1.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz#57ba1b0cbda8e7a3c597a4853c807b156e21a7b4" - integrity sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q== - dependencies: - "@types/estree" "^1.0.0" - estree-walker "^2.0.2" - picomatch "^4.0.2" - "@segment/analytics-core@1.8.2": version "1.8.2" resolved "https://registry.yarnpkg.com/@segment/analytics-core/-/analytics-core-1.8.2.tgz#f8aea312af3655e6483d0bdc160f0e05484529ab" @@ -4278,11 +4267,6 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -"@yarnpkg/lockfile@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" - integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== - acorn-import-phases@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" @@ -5143,7 +5127,7 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== -ci-info@^3.2.0, ci-info@^3.7.0: +ci-info@^3.2.0: version "3.9.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== @@ -5987,9 +5971,9 @@ dexie@^3.2.2: integrity sha512-2a+BXvVhY5op+smDRLxeBAivE7YcYaneXJ1la3HOkUfX9zKkE/AJ8CNgjiXbtXepFyFmJNGSbmjOwqbT749r/w== dexie@^4.0.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/dexie/-/dexie-4.2.1.tgz#70d111ae8d2dabf53f424fca79f6f918c407e6db" - integrity sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg== + version "4.4.2" + resolved "https://registry.yarnpkg.com/dexie/-/dexie-4.4.2.tgz#447511328c982baaf6a88c736ddc08484916886c" + integrity sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw== diff@^4.0.1: version "4.0.4" @@ -6758,11 +6742,6 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== -estree-walker@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" - integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== - estree-walker@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" @@ -6990,13 +6969,6 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -find-yarn-workspace-root@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" - integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== - dependencies: - micromatch "^4.0.2" - flat-cache@^3.0.4: version "3.2.0" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" @@ -7331,7 +7303,7 @@ gopd@^1.0.1, gopd@^1.2.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -7418,7 +7390,7 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" -he@1.2.0, he@^1.2.0: +he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -7964,7 +7936,7 @@ is-weakset@^2.0.3: call-bound "^1.0.3" get-intrinsic "^1.2.6" -is-wsl@^2.1.1, is-wsl@^2.2.0: +is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -8565,17 +8537,6 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json-stable-stringify@^1.0.2: - version "1.3.0" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz#8903cfac42ea1a0f97f35d63a4ce0518f0cc6a70" - integrity sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.4" - isarray "^2.0.5" - jsonify "^0.0.1" - object-keys "^1.1.1" - json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" @@ -8602,11 +8563,6 @@ jsonfile@^6.0.1, jsonfile@^6.1.0: optionalDependencies: graceful-fs "^4.1.6" -jsonify@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" - integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== - "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1: version "3.3.5" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" @@ -8636,13 +8592,6 @@ kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -klaw-sync@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" - integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== - dependencies: - graceful-fs "^4.1.11" - kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -9614,14 +9563,6 @@ node-fetch@^2.6.7, node-fetch@^2.7.0: dependencies: whatwg-url "^5.0.0" -node-html-parser@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-7.1.0.tgz#48e83bd705dd8b89415c00bf670aebfe267312d7" - integrity sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ== - dependencies: - css-select "^5.1.0" - he "1.2.0" - node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -9785,14 +9726,6 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -open@^7.4.2: - version "7.4.2" - resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" - integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== - dependencies: - is-docker "^2.0.0" - is-wsl "^2.1.1" - open@^8.0.4, open@^8.4.0: version "8.4.2" resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" @@ -9962,26 +9895,6 @@ pascal-case@^3.1.2: no-case "^3.0.4" tslib "^2.0.3" -patch-package@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.1.tgz#79d02f953f711e06d1f8949c8a13e5d3d7ba1a60" - integrity sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw== - dependencies: - "@yarnpkg/lockfile" "^1.1.0" - chalk "^4.1.2" - ci-info "^3.7.0" - cross-spawn "^7.0.3" - find-yarn-workspace-root "^2.0.0" - fs-extra "^10.0.0" - json-stable-stringify "^1.0.2" - klaw-sync "^6.0.0" - minimist "^1.2.6" - open "^7.4.2" - semver "^7.5.3" - slash "^2.0.0" - tmp "^0.2.4" - yaml "^2.2.2" - path-browserify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" @@ -10662,11 +10575,6 @@ postcss@^8.3.5, postcss@^8.4.33, postcss@^8.4.41, postcss@^8.5.6: picocolors "^1.1.1" source-map-js "^1.2.1" -postinstall-postinstall@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" - integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -11706,11 +11614,6 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -slash@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" - integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== - slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -12300,11 +12203,6 @@ tldts@^6.1.32: dependencies: tldts-core "^6.1.86" -tmp@^0.2.4: - version "0.2.5" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" - integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== - tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -13269,11 +13167,6 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yaml@^2.2.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d" - integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg== - yargs-parser@^21.0.0, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" @@ -13326,9 +13219,9 @@ zlib@^1.0.5: integrity sha512-40fpE2II+Cd3k8HWTWONfeKE2jL+P42iWJ1zzps5W51qcTsOUKM5Q5m2PFb0CLxlmFAaUuUdJGc3OfZy947v0w== zustand@^5.0.0: - version "5.0.12" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.12.tgz#ed36f647aa89965c4019b671dfc23ef6c6e3af8c" - integrity sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g== + version "5.0.11" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.11.tgz#99f912e590de1ca9ce6c6d1cab6cdb1f034ab494" + integrity sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg== zustand@^5.0.9: version "5.0.10"