mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
SEL-444: Fix android cloud backup (#697)
* feat(android): migrate google backup * update lock and google services config * add bulk format command * backup fixes * working drive settings!!!!!!!! * remove unneeded intent filter * add tests * coderabbit feedback * coderabbit feedback * abstract google method * coderabbit feedback and fix test * more coderabbit suggestions and tests fixes
This commit is contained in:
@@ -88,6 +88,7 @@ android {
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 78
|
||||
versionName "2.5.5"
|
||||
manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp']
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cppFlags += "-fexceptions -frtti -std=c++11"
|
||||
|
||||
@@ -12,7 +12,23 @@
|
||||
"package_name": "com.proofofpassportapp"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "148489200613-m7f49n6qav3ufcvai9j9k2dp4v0671qk.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.proofofpassportapp",
|
||||
"certificate_hash": [
|
||||
"88:5D:A6:EF:1D:DA:05:C7:F0:55:F1:F4:C4:16:4C:6E:93:4E:76:78",
|
||||
"AB:FE:10:57:76:E5:F9:FC:4F:95:6B:59:FC:78:7A:4D:D6:47:DA:BC"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "148489200613-grc1n6g4aspl2m5euk9neqagf4atnrs5.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDbMZ52EpHA-5BbMgTQK3OMOVcarGJQ-es"
|
||||
|
||||
@@ -31,26 +31,27 @@
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="redirect.self.xyz" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.nfc.action.TECH_DISCOVERED"
|
||||
android:resource="@xml/nfc_tech_filter" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="net.openid.appauth.RedirectUriReceiverActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="${appAuthRedirectScheme}" android:host="oauth2redirect" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name="com.google.firebase.messaging.FirebaseMessagingService"
|
||||
android:exported="false">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
PODS:
|
||||
- AppAuth (1.7.6):
|
||||
- AppAuth/Core (= 1.7.6)
|
||||
- AppAuth/ExternalUserAgent (= 1.7.6)
|
||||
- AppAuth/Core (1.7.6)
|
||||
- AppAuth/ExternalUserAgent (1.7.6):
|
||||
- AppAuth (2.0.0):
|
||||
- AppAuth/Core (= 2.0.0)
|
||||
- AppAuth/ExternalUserAgent (= 2.0.0)
|
||||
- AppAuth/Core (2.0.0)
|
||||
- AppAuth/ExternalUserAgent (2.0.0):
|
||||
- AppAuth/Core
|
||||
- boost (1.84.0)
|
||||
- DoubleConversion (1.1.6)
|
||||
@@ -84,10 +84,6 @@ PODS:
|
||||
- GoogleUtilities/Environment (~> 7.7)
|
||||
- nanopb (< 2.30911.0, >= 2.30908.0)
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- GoogleSignIn (7.1.0):
|
||||
- AppAuth (< 2.0, >= 1.7.3)
|
||||
- GTMAppAuth (< 5.0, >= 4.1.1)
|
||||
- GTMSessionFetcher/Core (~> 3.3)
|
||||
- GoogleUtilities (7.13.3):
|
||||
- GoogleUtilities/AppDelegateSwizzler (= 7.13.3)
|
||||
- GoogleUtilities/Environment (= 7.13.3)
|
||||
@@ -132,10 +128,6 @@ PODS:
|
||||
- GoogleUtilities/UserDefaults (7.13.3):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GTMAppAuth (4.1.1):
|
||||
- AppAuth/Core (~> 1.7)
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
|
||||
- GTMSessionFetcher/Core (3.5.0)
|
||||
- lottie-ios (4.5.0)
|
||||
- lottie-react-native (7.2.2):
|
||||
- DoubleConversion
|
||||
@@ -1381,6 +1373,9 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- react-native-app-auth (8.0.3):
|
||||
- AppAuth (>= 1.7.6)
|
||||
- React-Core
|
||||
- react-native-biometrics (3.0.1):
|
||||
- React-Core
|
||||
- react-native-cloud-storage (2.3.0):
|
||||
@@ -1715,9 +1710,6 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNGoogleSignin (13.3.1):
|
||||
- GoogleSignIn (~> 7.1)
|
||||
- React-Core
|
||||
- RNKeychain (10.0.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
@@ -1870,6 +1862,7 @@ DEPENDENCIES:
|
||||
- React-logger (from `../../node_modules/react-native/ReactCommon/logger`)
|
||||
- React-Mapbuffer (from `../../node_modules/react-native/ReactCommon`)
|
||||
- React-microtasksnativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
||||
- react-native-app-auth (from `../../node_modules/react-native-app-auth`)
|
||||
- react-native-biometrics (from `../../node_modules/react-native-biometrics`)
|
||||
- react-native-cloud-storage (from `../../node_modules/react-native-cloud-storage`)
|
||||
- react-native-get-random-values (from `../../node_modules/react-native-get-random-values`)
|
||||
@@ -1909,7 +1902,6 @@ DEPENDENCIES:
|
||||
- "RNFBApp (from `../../node_modules/@react-native-firebase/app`)"
|
||||
- "RNFBMessaging (from `../../node_modules/@react-native-firebase/messaging`)"
|
||||
- RNGestureHandler (from `../../node_modules/react-native-gesture-handler`)
|
||||
- "RNGoogleSignin (from `../../node_modules/@react-native-google-signin/google-signin`)"
|
||||
- RNKeychain (from `../../node_modules/react-native-keychain`)
|
||||
- RNLocalize (from `../../node_modules/react-native-localize`)
|
||||
- RNReactNativeHapticFeedback (from `../../node_modules/react-native-haptic-feedback`)
|
||||
@@ -1935,10 +1927,7 @@ SPEC REPOS:
|
||||
- FirebaseMessaging
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleSignIn
|
||||
- GoogleUtilities
|
||||
- GTMAppAuth
|
||||
- GTMSessionFetcher
|
||||
- lottie-ios
|
||||
- Mixpanel-swift
|
||||
- nanopb
|
||||
@@ -2025,6 +2014,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/react-native/ReactCommon"
|
||||
React-microtasksnativemodule:
|
||||
:path: "../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
||||
react-native-app-auth:
|
||||
:path: "../../node_modules/react-native-app-auth"
|
||||
react-native-biometrics:
|
||||
:path: "../../node_modules/react-native-biometrics"
|
||||
react-native-cloud-storage:
|
||||
@@ -2103,8 +2094,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/@react-native-firebase/messaging"
|
||||
RNGestureHandler:
|
||||
:path: "../../node_modules/react-native-gesture-handler"
|
||||
RNGoogleSignin:
|
||||
:path: "../../node_modules/@react-native-google-signin/google-signin"
|
||||
RNKeychain:
|
||||
:path: "../../node_modules/react-native-keychain"
|
||||
RNLocalize:
|
||||
@@ -2135,7 +2124,7 @@ CHECKOUT OPTIONS:
|
||||
:git: https://github.com/vinodiOS/SwiftQRScanner
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
|
||||
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
|
||||
boost: 4cb898d0bf20404aab1850c656dcea009429d6c1
|
||||
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
|
||||
FBLazyVector: 430e10366de01d1e3d57374500b1b150fe482e6d
|
||||
@@ -2150,10 +2139,7 @@ SPEC CHECKSUMS:
|
||||
glog: 69ef571f3de08433d766d614c73a9838a06bf7eb
|
||||
GoogleAppMeasurement: f3abf08495ef2cba7829f15318c373b8d9226491
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db
|
||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
||||
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
|
||||
lottie-react-native: 3ffec00c889acded6057766c99adf8eaced7790c
|
||||
Mixpanel-swift: e9bef28a9648faff384d5ba6f48ecc2787eb24c0
|
||||
@@ -2192,6 +2178,7 @@ SPEC CHECKSUMS:
|
||||
React-logger: 80d87daf2f98bf95ab668b79062c1e0c3f0c2f8a
|
||||
React-Mapbuffer: b2642edd9be75d51ead8cda109c986665eae09cf
|
||||
React-microtasksnativemodule: 7ebf131e1792a668004d2719a36da0ff8d19c43c
|
||||
react-native-app-auth: eb42594042a25455119a8c57194b4fd25b9352f4
|
||||
react-native-biometrics: 43ed5b828646a7862dbc7945556446be00798e7d
|
||||
react-native-cloud-storage: 47e6649373f429d2f244c904347e9fc4d9ef4ca8
|
||||
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
|
||||
@@ -2231,7 +2218,6 @@ SPEC CHECKSUMS:
|
||||
RNFBApp: 4097f75673f8b42a7cd1ba17e6ea85a94b45e4d1
|
||||
RNFBMessaging: 92325b0d5619ac90ef023a23cfd16fd3b91d0a88
|
||||
RNGestureHandler: 9c3877d98d4584891b69d16ebca855ac46507f4d
|
||||
RNGoogleSignin: 576a84dd0407b912e7a0adf07492de9feb79e5d9
|
||||
RNKeychain: 4990d9be2916c60f9ed4f8c484fcd7ced4828b86
|
||||
RNLocalize: 15463c4d79c7da45230064b4adcf5e9bb984667e
|
||||
RNReactNativeHapticFeedback: e19b9b2e2ecf5593de8c4ef1496e1e31ae227514
|
||||
|
||||
@@ -199,35 +199,48 @@ jest.mock('@stablelib/utf8', () => ({
|
||||
decode: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock @react-native-google-signin/google-signin
|
||||
jest.mock('@react-native-google-signin/google-signin', () => ({
|
||||
GoogleSignin: {
|
||||
configure: jest.fn(),
|
||||
hasPlayServices: jest.fn().mockResolvedValue(true),
|
||||
signIn: jest.fn().mockResolvedValue({
|
||||
user: {
|
||||
id: 'mock-user-id',
|
||||
email: 'mock@example.com',
|
||||
name: 'Mock User',
|
||||
photo: 'mock-photo-url',
|
||||
},
|
||||
}),
|
||||
signOut: jest.fn(),
|
||||
revokeAccess: jest.fn(),
|
||||
isSignedIn: jest.fn().mockResolvedValue(false),
|
||||
getCurrentUser: jest.fn().mockResolvedValue(null),
|
||||
getTokens: jest.fn().mockResolvedValue({
|
||||
accessToken: 'mock-access-token',
|
||||
idToken: 'mock-id-token',
|
||||
}),
|
||||
},
|
||||
statusCodes: {
|
||||
SIGN_IN_REQUIRED: 'SIGN_IN_REQUIRED',
|
||||
IN_PROGRESS: 'IN_PROGRESS',
|
||||
PLAY_SERVICES_NOT_AVAILABLE: 'PLAY_SERVICES_NOT_AVAILABLE',
|
||||
},
|
||||
// Mock react-native-app-auth
|
||||
jest.mock('react-native-app-auth', () => ({
|
||||
authorize: jest.fn().mockResolvedValue({ accessToken: 'mock-access-token' }),
|
||||
}));
|
||||
|
||||
// Mock @robinbobin/react-native-google-drive-api-wrapper
|
||||
jest.mock('@robinbobin/react-native-google-drive-api-wrapper', () => {
|
||||
class MockUploader {
|
||||
setData() {
|
||||
return this;
|
||||
}
|
||||
setDataMimeType() {
|
||||
return this;
|
||||
}
|
||||
setRequestBody() {
|
||||
return this;
|
||||
}
|
||||
execute = jest.fn();
|
||||
}
|
||||
|
||||
class MockFiles {
|
||||
newMultipartUploader() {
|
||||
return new MockUploader();
|
||||
}
|
||||
list = jest.fn().mockResolvedValue({ files: [] });
|
||||
delete = jest.fn();
|
||||
getText = jest.fn().mockResolvedValue('');
|
||||
}
|
||||
|
||||
class GDrive {
|
||||
accessToken = '';
|
||||
files = new MockFiles();
|
||||
}
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
GDrive,
|
||||
MIME_TYPES: { application: { json: 'application/json' } },
|
||||
APP_DATA_FOLDER_ID: 'appDataFolder',
|
||||
};
|
||||
});
|
||||
|
||||
// Mock react-native-cloud-storage
|
||||
jest.mock('react-native-cloud-storage', () => {
|
||||
const mockCloudStorage = {
|
||||
|
||||
@@ -33,17 +33,18 @@
|
||||
"force-local-upload-test": "yarn force-local-upload-test:android && yarn force-local-upload-test:ios",
|
||||
"force-local-upload-test:android": "yarn reinstall && FORCE_UPLOAD_LOCAL_DEV=true bundle exec fastlane android internal_test --verbose",
|
||||
"force-local-upload-test:ios": "yarn reinstall && FORCE_UPLOAD_LOCAL_DEV=true bundle exec fastlane ios internal_test --verbose",
|
||||
"format": "yarn nice",
|
||||
"ia": "yarn install-app",
|
||||
"install-app": "yarn install-app:setup && cd ios && bundle exec pod install && cd .. && yarn clean:xcode-env-local",
|
||||
"install-app:deploy": "yarn install-app:setup && yarn clean:xcode-env-local",
|
||||
"install-app:setup": "yarn install && yarn build:deps && cd ios && bundle install && cd ..",
|
||||
"ios": "react-native run-ios",
|
||||
"ios:fastlane-debug": "yarn reinstall && bundle exec fastlane --verbose ios internal_test",
|
||||
"setup": "yarn clean:build && yarn install && yarn build:deps && cd ios && bundle install && bundle exec pod install --repo-update && cd .. && yarn clean:xcode-env-local",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"nice": "yarn lint:fix && yarn fmt:fix",
|
||||
"reinstall": "yarn clean && yarn install && yarn install-app",
|
||||
"setup": "yarn clean:build && yarn install && yarn build:deps && cd ios && bundle install && bundle exec pod install --repo-update && cd .. && yarn clean:xcode-env-local",
|
||||
"start": "watchman watch-del-all && react-native start",
|
||||
"sync-versions": "bundle exec fastlane ios sync_version && bundle exec fastlane android sync_version",
|
||||
"tag:release": "node scripts/tag.js release",
|
||||
@@ -62,9 +63,9 @@
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
"@react-native-firebase/app": "^19.0.1",
|
||||
"@react-native-firebase/messaging": "^19.0.1",
|
||||
"@react-native-google-signin/google-signin": "^13.1.0",
|
||||
"@react-navigation/native": "^7.0.14",
|
||||
"@react-navigation/native-stack": "^7.2.0",
|
||||
"@robinbobin/react-native-google-drive-api-wrapper": "^2.2.3",
|
||||
"@segment/analytics-react-native": "^2.21.0",
|
||||
"@segment/sovran-react-native": "^1.1.3",
|
||||
"@selfxyz/common": "workspace:^",
|
||||
@@ -90,6 +91,7 @@
|
||||
"poseidon-lite": "^0.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-native": "0.75.4",
|
||||
"react-native-app-auth": "^8.0.3",
|
||||
"react-native-biometrics": "^3.0.1",
|
||||
"react-native-check-version": "^1.3.0",
|
||||
"react-native-cloud-storage": "^2.2.2",
|
||||
|
||||
@@ -1,39 +1,42 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11
|
||||
|
||||
import { GOOGLE_SIGNIN_WEB_CLIENT_ID } from '@env';
|
||||
import { GOOGLE_SIGNIN_ANDROID_CLIENT_ID } from '@env';
|
||||
import { GDrive } from '@robinbobin/react-native-google-drive-api-wrapper';
|
||||
import {
|
||||
GoogleSignin,
|
||||
isErrorWithCode,
|
||||
statusCodes,
|
||||
} from '@react-native-google-signin/google-signin';
|
||||
AuthConfiguration,
|
||||
authorize,
|
||||
AuthorizeResult,
|
||||
} from 'react-native-app-auth';
|
||||
|
||||
GoogleSignin.configure({
|
||||
const config: AuthConfiguration = {
|
||||
// DEBUG: log config for Auth
|
||||
// ensure this prints the correct values before calling authorize
|
||||
clientId: GOOGLE_SIGNIN_ANDROID_CLIENT_ID,
|
||||
redirectUrl: 'com.proofofpassportapp:/oauth2redirect',
|
||||
scopes: ['https://www.googleapis.com/auth/drive.appdata'],
|
||||
webClientId: GOOGLE_SIGNIN_WEB_CLIENT_ID,
|
||||
offlineAccess: true,
|
||||
});
|
||||
serviceConfiguration: {
|
||||
authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
|
||||
tokenEndpoint: 'https://oauth2.googleapis.com/token',
|
||||
},
|
||||
additionalParameters: { access_type: 'offline', prompt: 'consent' as const },
|
||||
};
|
||||
|
||||
export async function googleSignIn() {
|
||||
export async function googleSignIn(): Promise<AuthorizeResult | null> {
|
||||
try {
|
||||
await GoogleSignin.hasPlayServices();
|
||||
if ((await GoogleSignin.signInSilently()).type === 'success') {
|
||||
return await GoogleSignin.getTokens();
|
||||
}
|
||||
if ((await GoogleSignin.signIn()).type === 'success') {
|
||||
return await GoogleSignin.getTokens();
|
||||
}
|
||||
// user cancelled
|
||||
return null;
|
||||
return await authorize(config);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (isErrorWithCode(error)) {
|
||||
switch (error.code) {
|
||||
case statusCodes.IN_PROGRESS:
|
||||
return null;
|
||||
case statusCodes.PLAY_SERVICES_NOT_AVAILABLE:
|
||||
throw new Error('GooglePlayServices not available');
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createGDrive() {
|
||||
const response = await googleSignIn();
|
||||
if (!response) {
|
||||
// user canceled
|
||||
return null;
|
||||
}
|
||||
const gdrive = new GDrive();
|
||||
gdrive.accessToken = response.accessToken;
|
||||
return gdrive;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,73 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11
|
||||
|
||||
import {
|
||||
APP_DATA_FOLDER_ID,
|
||||
MIME_TYPES,
|
||||
} from '@robinbobin/react-native-google-drive-api-wrapper';
|
||||
import { ethers } from 'ethers';
|
||||
import { useMemo } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import {
|
||||
CloudStorage,
|
||||
CloudStorageProvider,
|
||||
CloudStorageScope,
|
||||
} from 'react-native-cloud-storage';
|
||||
import { CloudStorage, CloudStorageScope } from 'react-native-cloud-storage';
|
||||
|
||||
import { name } from '../../../package.json';
|
||||
import { Mnemonic } from '../../types/mnemonic';
|
||||
import { googleSignIn } from './google';
|
||||
import { createGDrive } from './google';
|
||||
|
||||
const FOLDER = `/${name}`;
|
||||
const ENCRYPTED_FILE_PATH = `/${FOLDER}/encrypted-private-key`;
|
||||
CloudStorage.setProviderOptions({ scope: CloudStorageScope.AppData });
|
||||
if (Platform.OS === 'ios') {
|
||||
CloudStorage.setProviderOptions({ scope: CloudStorageScope.AppData });
|
||||
}
|
||||
|
||||
const FILE_NAME = 'encrypted-private-key';
|
||||
|
||||
export const STORAGE_NAME = Platform.OS === 'ios' ? 'iCloud' : 'Google Drive';
|
||||
|
||||
/**
|
||||
* Type guard function to validate that an object conforms to the Mnemonic interface
|
||||
*/
|
||||
function isMnemonic(obj: unknown): obj is Mnemonic {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidate = obj as Record<string, unknown>;
|
||||
|
||||
return !!(
|
||||
typeof candidate.phrase === 'string' &&
|
||||
typeof candidate.password === 'string' &&
|
||||
typeof candidate.entropy === 'string' &&
|
||||
candidate.wordlist &&
|
||||
typeof candidate.wordlist === 'object' &&
|
||||
typeof (candidate.wordlist as Record<string, unknown>).locale === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parses and validates a mnemonic string
|
||||
*/
|
||||
function parseMnemonic(mnemonicString: string): Mnemonic {
|
||||
let parsed: unknown;
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(mnemonicString);
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON format in mnemonic backup');
|
||||
}
|
||||
|
||||
if (!isMnemonic(parsed)) {
|
||||
throw new Error(
|
||||
'Invalid mnemonic structure: missing required properties (phrase, password, wordlist, entropy)',
|
||||
);
|
||||
}
|
||||
|
||||
if (!parsed.phrase || !ethers.Mnemonic.isValidMnemonic(parsed.phrase)) {
|
||||
throw new Error('Invalid mnemonic phrase: not a valid BIP39 mnemonic');
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* For some reason google drive api can be very ... brittle and abort randomly (network conditions)
|
||||
* so retry a couple times for good measure.
|
||||
@@ -59,68 +108,100 @@ export function useBackupMnemonic() {
|
||||
);
|
||||
}
|
||||
|
||||
async function addAccessTokenForGoogleDrive() {
|
||||
if (CloudStorage.getProvider() === CloudStorageProvider.GoogleDrive) {
|
||||
const response = await googleSignIn();
|
||||
if (!response) {
|
||||
// user canceled
|
||||
return;
|
||||
}
|
||||
CloudStorage.setProviderOptions({
|
||||
accessToken: response.accessToken,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function upload(mnemonic: Mnemonic) {
|
||||
export async function upload(mnemonic: Mnemonic) {
|
||||
if (!mnemonic || !mnemonic.phrase) {
|
||||
throw new Error(
|
||||
'Mnemonic not set yet. Did the user see the recovery phrase?',
|
||||
);
|
||||
}
|
||||
|
||||
await addAccessTokenForGoogleDrive();
|
||||
try {
|
||||
await CloudStorage.mkdir(FOLDER);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (!(e as Error).message.includes('already')) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
await withRetries(() =>
|
||||
CloudStorage.writeFile(ENCRYPTED_FILE_PATH, JSON.stringify(mnemonic)),
|
||||
);
|
||||
}
|
||||
|
||||
async function download() {
|
||||
await addAccessTokenForGoogleDrive();
|
||||
if (await CloudStorage.exists(ENCRYPTED_FILE_PATH)) {
|
||||
const mnemonicString = await withRetries(() =>
|
||||
CloudStorage.readFile(ENCRYPTED_FILE_PATH),
|
||||
);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
try {
|
||||
const mnemonic = JSON.parse(mnemonicString) as Mnemonic;
|
||||
if (
|
||||
!mnemonic.phrase ||
|
||||
!ethers.Mnemonic.isValidMnemonic(mnemonic.phrase)
|
||||
) {
|
||||
throw new Error();
|
||||
}
|
||||
return mnemonic;
|
||||
await CloudStorage.mkdir(FOLDER);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Malformed mnemonic, expected JSON structure, got ${mnemonicString}`,
|
||||
);
|
||||
console.error(e);
|
||||
if (!(e as Error).message.includes('already')) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
await withRetries(() =>
|
||||
CloudStorage.writeFile(ENCRYPTED_FILE_PATH, JSON.stringify(mnemonic)),
|
||||
);
|
||||
} else {
|
||||
const gdrive = await createGDrive();
|
||||
if (!gdrive) {
|
||||
throw new Error('User canceled Google sign-in');
|
||||
}
|
||||
await withRetries(() =>
|
||||
gdrive.files
|
||||
.newMultipartUploader()
|
||||
.setData(JSON.stringify(mnemonic))
|
||||
.setDataMimeType(MIME_TYPES.application.json)
|
||||
.setRequestBody({ name: FILE_NAME, parents: [APP_DATA_FOLDER_ID] })
|
||||
.execute(),
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
'Couldnt find the encrypted backup, did you back it up previously?',
|
||||
);
|
||||
}
|
||||
|
||||
async function disableBackup() {
|
||||
await addAccessTokenForGoogleDrive();
|
||||
withRetries(() => CloudStorage.rmdir(FOLDER, { recursive: true }));
|
||||
export async function download() {
|
||||
if (Platform.OS === 'ios') {
|
||||
if (await CloudStorage.exists(ENCRYPTED_FILE_PATH)) {
|
||||
const mnemonicString = await withRetries(() =>
|
||||
CloudStorage.readFile(ENCRYPTED_FILE_PATH),
|
||||
);
|
||||
|
||||
try {
|
||||
const mnemonic = parseMnemonic(mnemonicString);
|
||||
return mnemonic;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to parse mnemonic backup: ${(e as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
'Couldnt find the encrypted backup, did you back it up previously?',
|
||||
);
|
||||
}
|
||||
|
||||
const gdrive = await createGDrive();
|
||||
if (!gdrive) {
|
||||
throw new Error('User canceled Google sign-in');
|
||||
}
|
||||
const { files } = await gdrive.files.list({
|
||||
spaces: APP_DATA_FOLDER_ID,
|
||||
q: `name = '${FILE_NAME}'`,
|
||||
});
|
||||
if (!files.length || !files[0].id) {
|
||||
throw new Error(
|
||||
'Couldnt find the encrypted backup, did you back it up previously?',
|
||||
);
|
||||
}
|
||||
const mnemonicString = await withRetries(() =>
|
||||
gdrive.files.getText(files[0].id as string),
|
||||
);
|
||||
try {
|
||||
const mnemonic = parseMnemonic(mnemonicString);
|
||||
return mnemonic;
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to parse mnemonic backup: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function disableBackup() {
|
||||
if (Platform.OS === 'ios') {
|
||||
await withRetries(() => CloudStorage.rmdir(FOLDER, { recursive: true }));
|
||||
return;
|
||||
}
|
||||
const gdrive = await createGDrive();
|
||||
if (!gdrive) {
|
||||
// User canceled Google sign-in; skip disabling backup gracefully.
|
||||
return;
|
||||
}
|
||||
const { files } = await gdrive.files.list({
|
||||
spaces: APP_DATA_FOLDER_ID,
|
||||
q: `name = '${FILE_NAME}'`,
|
||||
});
|
||||
await Promise.all(
|
||||
files.filter(f => f.id).map(f => gdrive.files.delete(f.id as string)),
|
||||
);
|
||||
}
|
||||
|
||||
402
app/tests/utils/cloudBackup.test.ts
Normal file
402
app/tests/utils/cloudBackup.test.ts
Normal file
@@ -0,0 +1,402 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11
|
||||
|
||||
import { renderHook } from '@testing-library/react-native';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('react-native-cloud-storage', () => ({
|
||||
CloudStorage: {
|
||||
setProviderOptions: jest.fn(),
|
||||
mkdir: jest.fn(),
|
||||
writeFile: jest.fn(),
|
||||
exists: jest.fn(),
|
||||
readFile: jest.fn(),
|
||||
rmdir: jest.fn(),
|
||||
},
|
||||
CloudStorageScope: {
|
||||
AppData: 'AppData',
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@robinbobin/react-native-google-drive-api-wrapper', () => ({
|
||||
GDrive: jest.fn(),
|
||||
APP_DATA_FOLDER_ID: 'mock-app-data-folder',
|
||||
MIME_TYPES: {
|
||||
application: {
|
||||
json: 'application/json',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../src/utils/cloudBackup/google', () => {
|
||||
const originalModule = jest.requireActual(
|
||||
'../../src/utils/cloudBackup/google',
|
||||
);
|
||||
return {
|
||||
...originalModule,
|
||||
createGDrive: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('ethers', () => ({
|
||||
ethers: {
|
||||
Mnemonic: {
|
||||
isValidMnemonic: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Import after mocks
|
||||
import { GDrive } from '@robinbobin/react-native-google-drive-api-wrapper';
|
||||
import { ethers } from 'ethers';
|
||||
import { CloudStorage } from 'react-native-cloud-storage';
|
||||
|
||||
import { useBackupMnemonic } from '../../src/utils/cloudBackup';
|
||||
import { createGDrive } from '../../src/utils/cloudBackup/google';
|
||||
|
||||
// Mock implementations
|
||||
const mockGDriveInstance = {
|
||||
accessToken: '',
|
||||
files: {
|
||||
newMultipartUploader: jest.fn().mockReturnValue({
|
||||
setData: jest.fn().mockReturnThis(),
|
||||
setDataMimeType: jest.fn().mockReturnThis(),
|
||||
setRequestBody: jest.fn().mockReturnThis(),
|
||||
execute: jest.fn(),
|
||||
}),
|
||||
list: jest.fn(),
|
||||
getText: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const mockMnemonic = {
|
||||
phrase:
|
||||
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
||||
password: '',
|
||||
wordlist: { locale: 'en' },
|
||||
entropy: '0x00000000000000000000000000000000',
|
||||
};
|
||||
|
||||
describe('cloudBackup', () => {
|
||||
let originalPlatform: any;
|
||||
let consoleSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
originalPlatform = Platform.OS;
|
||||
// Suppress console.error during tests to avoid cluttering output
|
||||
consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
(GDrive as jest.Mock).mockImplementation(() => mockGDriveInstance);
|
||||
(ethers.Mnemonic.isValidMnemonic as jest.Mock).mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Platform.OS = originalPlatform;
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('useBackupMnemonic hook', () => {
|
||||
it('should return upload, download, and disableBackup functions', () => {
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
expect(result.current).toHaveProperty('upload');
|
||||
expect(result.current).toHaveProperty('download');
|
||||
expect(result.current).toHaveProperty('disableBackup');
|
||||
expect(typeof result.current.upload).toBe('function');
|
||||
expect(typeof result.current.download).toBe('function');
|
||||
expect(typeof result.current.disableBackup).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('upload function - iOS', () => {
|
||||
beforeEach(() => {
|
||||
Platform.OS = 'ios';
|
||||
});
|
||||
|
||||
it('should upload mnemonic to iCloud successfully', async () => {
|
||||
(CloudStorage.mkdir as jest.Mock).mockResolvedValue(undefined);
|
||||
(CloudStorage.writeFile as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(
|
||||
result.current.upload(mockMnemonic),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(CloudStorage.mkdir).toHaveBeenCalledWith('/@selfxyz/mobile-app');
|
||||
expect(CloudStorage.writeFile).toHaveBeenCalledWith(
|
||||
'//@selfxyz/mobile-app/encrypted-private-key',
|
||||
JSON.stringify(mockMnemonic),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle folder already exists error gracefully', async () => {
|
||||
const folderExistsError = new Error('folder already exists');
|
||||
(CloudStorage.mkdir as jest.Mock).mockRejectedValue(folderExistsError);
|
||||
(CloudStorage.writeFile as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(
|
||||
result.current.upload(mockMnemonic),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(CloudStorage.writeFile).toHaveBeenCalledWith(
|
||||
'//@selfxyz/mobile-app/encrypted-private-key',
|
||||
JSON.stringify(mockMnemonic),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for empty mnemonic', async () => {
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(
|
||||
result.current.upload({
|
||||
phrase: '',
|
||||
password: '',
|
||||
wordlist: { locale: 'en' },
|
||||
entropy: '',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
'Mnemonic not set yet. Did the user see the recovery phrase?',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for null mnemonic', async () => {
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(result.current.upload(null as any)).rejects.toThrow(
|
||||
'Mnemonic not set yet. Did the user see the recovery phrase?',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when mkdir fails with non-existing folder error', async () => {
|
||||
const permissionError = new Error('permission denied');
|
||||
(CloudStorage.mkdir as jest.Mock).mockRejectedValue(permissionError);
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(result.current.upload(mockMnemonic)).rejects.toThrow(
|
||||
'permission denied',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upload function - Android', () => {
|
||||
beforeEach(() => {
|
||||
Platform.OS = 'android';
|
||||
});
|
||||
|
||||
it('should upload mnemonic to Google Drive successfully', async () => {
|
||||
(createGDrive as jest.Mock).mockResolvedValue(mockGDriveInstance);
|
||||
mockGDriveInstance.files
|
||||
.newMultipartUploader()
|
||||
.execute.mockResolvedValue({});
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(
|
||||
result.current.upload(mockMnemonic),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(createGDrive).toHaveBeenCalled();
|
||||
expect(
|
||||
mockGDriveInstance.files.newMultipartUploader().setData,
|
||||
).toHaveBeenCalledWith(JSON.stringify(mockMnemonic));
|
||||
expect(
|
||||
mockGDriveInstance.files.newMultipartUploader().execute,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when user cancels Google sign-in', async () => {
|
||||
(createGDrive as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(result.current.upload(mockMnemonic)).rejects.toThrow(
|
||||
'User canceled Google sign-in',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('download function - iOS', () => {
|
||||
beforeEach(() => {
|
||||
Platform.OS = 'ios';
|
||||
});
|
||||
|
||||
it('should download and parse mnemonic from iCloud successfully', async () => {
|
||||
(CloudStorage.exists as jest.Mock).mockResolvedValue(true);
|
||||
(CloudStorage.readFile as jest.Mock).mockResolvedValue(
|
||||
JSON.stringify(mockMnemonic),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
const downloaded = await result.current.download();
|
||||
|
||||
expect(CloudStorage.exists).toHaveBeenCalledWith(
|
||||
'//@selfxyz/mobile-app/encrypted-private-key',
|
||||
);
|
||||
expect(CloudStorage.readFile).toHaveBeenCalledWith(
|
||||
'//@selfxyz/mobile-app/encrypted-private-key',
|
||||
);
|
||||
expect(downloaded).toEqual(mockMnemonic);
|
||||
expect(ethers.Mnemonic.isValidMnemonic).toHaveBeenCalledWith(
|
||||
mockMnemonic.phrase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when backup file does not exist', async () => {
|
||||
(CloudStorage.exists as jest.Mock).mockResolvedValue(false);
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(result.current.download()).rejects.toThrow(
|
||||
'Couldnt find the encrypted backup, did you back it up previously?',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for malformed mnemonic JSON', async () => {
|
||||
(CloudStorage.exists as jest.Mock).mockResolvedValue(true);
|
||||
(CloudStorage.readFile as jest.Mock).mockResolvedValue('invalid json');
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(result.current.download()).rejects.toThrow(
|
||||
'Failed to parse mnemonic backup: Invalid JSON format in mnemonic backup',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for invalid mnemonic phrase', async () => {
|
||||
const invalidMnemonic = { ...mockMnemonic, phrase: 'invalid phrase' };
|
||||
(CloudStorage.exists as jest.Mock).mockResolvedValue(true);
|
||||
(CloudStorage.readFile as jest.Mock).mockResolvedValue(
|
||||
JSON.stringify(invalidMnemonic),
|
||||
);
|
||||
(ethers.Mnemonic.isValidMnemonic as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(result.current.download()).rejects.toThrow(
|
||||
'Failed to parse mnemonic backup: Invalid mnemonic phrase: not a valid BIP39 mnemonic',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for missing mnemonic properties', async () => {
|
||||
const incompleteMnemonic = { phrase: 'valid phrase', password: '' }; // missing wordlist and entropy
|
||||
(CloudStorage.exists as jest.Mock).mockResolvedValue(true);
|
||||
(CloudStorage.readFile as jest.Mock).mockResolvedValue(
|
||||
JSON.stringify(incompleteMnemonic),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(result.current.download()).rejects.toThrow(
|
||||
'Failed to parse mnemonic backup: Invalid mnemonic structure: missing required properties (phrase, password, wordlist, entropy)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('download function - Android', () => {
|
||||
beforeEach(() => {
|
||||
Platform.OS = 'android';
|
||||
});
|
||||
|
||||
it('should download and parse mnemonic from Google Drive successfully', async () => {
|
||||
(createGDrive as jest.Mock).mockResolvedValue(mockGDriveInstance);
|
||||
mockGDriveInstance.files.list.mockResolvedValue({
|
||||
files: [{ id: 'file-id', name: 'encrypted-private-key' }],
|
||||
});
|
||||
mockGDriveInstance.files.getText.mockResolvedValue(
|
||||
JSON.stringify(mockMnemonic),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
const downloaded = await result.current.download();
|
||||
|
||||
expect(createGDrive).toHaveBeenCalled();
|
||||
expect(mockGDriveInstance.files.list).toHaveBeenCalledWith({
|
||||
spaces: 'mock-app-data-folder',
|
||||
q: "name = 'encrypted-private-key'",
|
||||
});
|
||||
expect(mockGDriveInstance.files.getText).toHaveBeenCalledWith('file-id');
|
||||
expect(downloaded).toEqual(mockMnemonic);
|
||||
expect(ethers.Mnemonic.isValidMnemonic).toHaveBeenCalledWith(
|
||||
mockMnemonic.phrase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when user cancels Google sign-in', async () => {
|
||||
(createGDrive as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(result.current.download()).rejects.toThrow(
|
||||
'User canceled Google sign-in',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when backup file does not exist', async () => {
|
||||
(createGDrive as jest.Mock).mockResolvedValue(mockGDriveInstance);
|
||||
mockGDriveInstance.files.list.mockResolvedValue({
|
||||
files: [],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(result.current.download()).rejects.toThrow(
|
||||
'Couldnt find the encrypted backup, did you back it up previously?',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for malformed mnemonic JSON', async () => {
|
||||
(createGDrive as jest.Mock).mockResolvedValue(mockGDriveInstance);
|
||||
mockGDriveInstance.files.list.mockResolvedValue({
|
||||
files: [{ id: 'file-id', name: 'encrypted-private-key' }],
|
||||
});
|
||||
mockGDriveInstance.files.getText.mockResolvedValue('invalid json');
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(result.current.download()).rejects.toThrow(
|
||||
'Failed to parse mnemonic backup: Invalid JSON format in mnemonic backup',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for invalid mnemonic phrase', async () => {
|
||||
(createGDrive as jest.Mock).mockResolvedValue(mockGDriveInstance);
|
||||
mockGDriveInstance.files.list.mockResolvedValue({
|
||||
files: [{ id: 'file-id', name: 'encrypted-private-key' }],
|
||||
});
|
||||
mockGDriveInstance.files.getText.mockResolvedValue(
|
||||
JSON.stringify({ ...mockMnemonic, phrase: 'invalid phrase' }),
|
||||
);
|
||||
(ethers.Mnemonic.isValidMnemonic as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(result.current.download()).rejects.toThrow(
|
||||
'Failed to parse mnemonic backup: Invalid mnemonic phrase: not a valid BIP39 mnemonic',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for missing mnemonic properties', async () => {
|
||||
const incompleteMnemonic = { phrase: 'valid phrase', password: '' }; // missing wordlist and entropy
|
||||
(createGDrive as jest.Mock).mockResolvedValue(mockGDriveInstance);
|
||||
mockGDriveInstance.files.list.mockResolvedValue({
|
||||
files: [{ id: 'file-id', name: 'encrypted-private-key' }],
|
||||
});
|
||||
mockGDriveInstance.files.getText.mockResolvedValue(
|
||||
JSON.stringify(incompleteMnemonic),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useBackupMnemonic());
|
||||
|
||||
await expect(result.current.download()).rejects.toThrow(
|
||||
'Failed to parse mnemonic backup: Invalid mnemonic structure: missing required properties (phrase, password, wordlist, entropy)',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "yarn workspaces foreach --topological-dev --parallel --exclude @selfxyz/contracts -i --all run build",
|
||||
"format": "yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run format",
|
||||
"lint": "yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run lint",
|
||||
"types": "yarn workspaces foreach --topological-dev --parallel --exclude @selfxyz/contracts -i --all run types ",
|
||||
"postinstall": "patch-package"
|
||||
|
||||
71
yarn.lock
71
yarn.lock
@@ -3567,23 +3567,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-native-google-signin/google-signin@npm:^13.1.0":
|
||||
version: 13.3.1
|
||||
resolution: "@react-native-google-signin/google-signin@npm:13.3.1"
|
||||
peerDependencies:
|
||||
expo: ">=50.0.0"
|
||||
react: "*"
|
||||
react-dom: "*"
|
||||
react-native: "*"
|
||||
peerDependenciesMeta:
|
||||
expo:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
checksum: 10c0/39be6c3bb52be30a8b6c49376ea90c325c07f63c01938df9ba4b3d8966f69c7e1e8fe8ab000227939b899fce06efd3725f3fba56848cf9d25eb9507f8f994f8c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-native/assets-registry@npm:0.75.4":
|
||||
version: 0.75.4
|
||||
resolution: "@react-native/assets-registry@npm:0.75.4"
|
||||
@@ -4019,6 +4002,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@robinbobin/mimetype-constants@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "@robinbobin/mimetype-constants@npm:2.0.1"
|
||||
dependencies:
|
||||
radashi: "npm:^12.2.3"
|
||||
checksum: 10c0/37aa993f596a32ba654baf99128752f86a616503a430d173d080701b041d5af24ea992f9d0429c26120c7f5b4d007e89f54735ce31850ae21dd2285646ab8ebc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@robinbobin/react-native-google-drive-api-wrapper@npm:^2.2.3":
|
||||
version: 2.2.3
|
||||
resolution: "@robinbobin/react-native-google-drive-api-wrapper@npm:2.2.3"
|
||||
dependencies:
|
||||
"@robinbobin/mimetype-constants": "npm:^2.0.1"
|
||||
base64-js: "npm:^1.5.1"
|
||||
radashi: "npm:^12.2.3"
|
||||
utf8: "npm:^3.0.0"
|
||||
checksum: 10c0/e6d26b3b72011ba2d4cc70d4a4f102ffefc1e52d4904b3ea3c9fbd97c2fc2d86d519270135cefb56e9f3de36e8a2c7471d276f63e09afe5666b4f10150b05a81
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@npm:4.44.0":
|
||||
version: 4.44.0
|
||||
resolution: "@rollup/rollup-android-arm-eabi@npm:4.44.0"
|
||||
@@ -4466,7 +4470,6 @@ __metadata:
|
||||
"@react-native-community/netinfo": "npm:^11.4.1"
|
||||
"@react-native-firebase/app": "npm:^19.0.1"
|
||||
"@react-native-firebase/messaging": "npm:^19.0.1"
|
||||
"@react-native-google-signin/google-signin": "npm:^13.1.0"
|
||||
"@react-native/babel-preset": "npm:0.75.4"
|
||||
"@react-native/eslint-config": "npm:0.75.4"
|
||||
"@react-native/gradle-plugin": "npm:^0.79.2"
|
||||
@@ -4474,6 +4477,7 @@ __metadata:
|
||||
"@react-native/typescript-config": "npm:0.75.4"
|
||||
"@react-navigation/native": "npm:^7.0.14"
|
||||
"@react-navigation/native-stack": "npm:^7.2.0"
|
||||
"@robinbobin/react-native-google-drive-api-wrapper": "npm:^2.2.3"
|
||||
"@segment/analytics-react-native": "npm:^2.21.0"
|
||||
"@segment/sovran-react-native": "npm:^1.1.3"
|
||||
"@selfxyz/common": "workspace:^"
|
||||
@@ -4522,6 +4526,7 @@ __metadata:
|
||||
prettier: "npm:^3.5.3"
|
||||
react: "npm:^18.3.1"
|
||||
react-native: "npm:0.75.4"
|
||||
react-native-app-auth: "npm:^8.0.3"
|
||||
react-native-biometrics: "npm:^3.0.1"
|
||||
react-native-bundle-visualizer: "npm:^3.1.3"
|
||||
react-native-check-version: "npm:^1.3.0"
|
||||
@@ -15068,7 +15073,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"invariant@npm:^2.2.4":
|
||||
"invariant@npm:2.2.4, invariant@npm:^2.2.4":
|
||||
version: 2.2.4
|
||||
resolution: "invariant@npm:2.2.4"
|
||||
dependencies:
|
||||
@@ -19388,6 +19393,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"radashi@npm:^12.2.3":
|
||||
version: 12.6.0
|
||||
resolution: "radashi@npm:12.6.0"
|
||||
checksum: 10c0/50bc1906bf13e13997d95ce0af7a452734b0973e4b9bdb73837789cf78656c1d91fadba2e8bcfb22c48b9d068408e54f01d86735589b77b35c50b3b0cbc21de6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"randombytes@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "randombytes@npm:2.1.0"
|
||||
@@ -19496,6 +19508,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-app-auth@npm:^8.0.3":
|
||||
version: 8.0.3
|
||||
resolution: "react-native-app-auth@npm:8.0.3"
|
||||
dependencies:
|
||||
invariant: "npm:2.2.4"
|
||||
react-native-base64: "npm:0.0.2"
|
||||
peerDependencies:
|
||||
react-native: ">=0.63.0"
|
||||
checksum: 10c0/a3b7e5394c509ad05fb091484d5ff0a14bf6e09b0cfa14c5d81b1abcd92b3e7110f17dca08cb4337e93725e78eddab4611c95a67ab2c7b8bec36984d8ae99999
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-base64@npm:0.0.2":
|
||||
version: 0.0.2
|
||||
resolution: "react-native-base64@npm:0.0.2"
|
||||
checksum: 10c0/655e5638c30bc0af1ac649ba053e849a268f12d9657124b1418e27019cbf0f4141e5a93086ee640b46e90e5bb1003d640f38d55504b3637fcd72b7894ac1a30c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-biometrics@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "react-native-biometrics@npm:3.0.1"
|
||||
@@ -23007,7 +23038,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"utf8@npm:3.0.0":
|
||||
"utf8@npm:3.0.0, utf8@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "utf8@npm:3.0.0"
|
||||
checksum: 10c0/675d008bab65fc463ce718d5cae8fd4c063540f269e4f25afebce643098439d53e7164bb1f193e0c3852825c7e3e32fbd8641163d19a618dbb53f1f09acb0d5a
|
||||
|
||||
Reference in New Issue
Block a user