fix backup on android and icloud

Co-authored-by: Leszek Stachowski <leszek.stachowski@clabs.co>
Co-authored-by: Aaron DeRuvo <aaron.deruvo@clabs.co>
This commit is contained in:
Nicolas Brugneaux
2025-02-14 20:01:20 +01:00
committed by GitHub
parent 5f71f9a392
commit 4a615a840f
22 changed files with 462 additions and 294 deletions

View File

@@ -16,8 +16,7 @@
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:allowBackup="false"
tools:replace="android:allowBackup, android:icon, android:roundIcon, android:name"
tools:replace="android:icon, android:roundIcon, android:name"
android:theme="@style/AppTheme"
android:supportsRtl="true">
<activity
@@ -43,11 +42,3 @@
</activity>
</application>
</manifest>
<!--
TODO: add this back to <application />
android:allowBackup="true"
android:backupInForeground="true"
android:dataExtractionRules="@xml/backup_rules"
-->

BIN
app/android/dev-keystore Normal file

Binary file not shown.

View File

@@ -4,14 +4,6 @@
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
@@ -20,6 +12,14 @@
<string>CA92.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
@@ -28,6 +28,14 @@
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>85F4.1</string>
</array>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array/>

View File

@@ -1,11 +1,25 @@
PODS:
- amplitude-react-native (1.4.11):
- React-Core
- 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/Core
- boost (1.84.0)
- DoubleConversion (1.1.6)
- FBLazyVector (0.75.4)
- fmt (9.1.0)
- glog (0.3.5)
- GoogleSignIn (7.1.0):
- AppAuth (< 2.0, >= 1.7.3)
- GTMAppAuth (< 5.0, >= 4.1.1)
- GTMSessionFetcher/Core (~> 3.3)
- 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
@@ -1527,6 +1541,8 @@ PODS:
- React-Core
- RNCClipboard (1.13.2):
- React-Core
- RNDeviceInfo (14.0.4):
- React-Core
- RNFS (2.20.0):
- React-Core
- RNGestureHandler (2.22.1):
@@ -1550,6 +1566,9 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNGoogleSignin (13.1.0):
- GoogleSignIn (~> 7.1)
- React-Core
- RNKeychain (8.2.0):
- React-Core
- RNLocalize (3.4.1):
@@ -1696,8 +1715,10 @@ DEPENDENCIES:
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
- RNFS (from `../node_modules/react-native-fs`)
- 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`)
@@ -1711,6 +1732,10 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- AppAuth
- GoogleSignIn
- GTMAppAuth
- GTMSessionFetcher
- lottie-ios
- OpenSSL-Universal
- QKMRZParser
@@ -1869,10 +1894,14 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-async-storage/async-storage"
RNCClipboard:
:path: "../node_modules/@react-native-clipboard/clipboard"
RNDeviceInfo:
:path: "../node_modules/react-native-device-info"
RNFS:
:path: "../node_modules/react-native-fs"
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:
@@ -1903,95 +1932,101 @@ CHECKOUT OPTIONS:
:git: https://github.com/vinodiOS/SwiftQRScanner
SPEC CHECKSUMS:
amplitude-react-native: 9d57e1bcc4175039e36283390aa3daeaea9441a5
amplitude-react-native: bf1634b1b263b460f69f6511db6e00332fa19a50
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
boost: 4cb898d0bf20404aab1850c656dcea009429d6c1
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
FBLazyVector: 430e10366de01d1e3d57374500b1b150fe482e6d
fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
glog: 69ef571f3de08433d766d614c73a9838a06bf7eb
GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
lottie-react-native: db03203d873afcbb6e0cea6a262a88d95e64a98f
lottie-react-native: 3ffec00c889acded6057766c99adf8eaced7790c
NFCPassportReader: e931c61c189e08a4b4afa0ed4014af19eab2f129
OpenSSL-Universal: 84efb8a29841f2764ac5403e0c4119a28b713346
QKMRZParser: 6b419b6f07d6bff6b50429b97de10846dc902c29
QKMRZScanner: cf2348fd6ce441e758328da4adf231ef2b51d769
RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740
RCT-Folly: 34124ae2e667a0e5f0ea378db071d27548124321
RCTDeprecation: 726d24248aeab6d7180dac71a936bbca6a994ed1
RCTRequired: a94e7febda6db0345d207e854323c37e3a31d93b
RCTTypeSafety: 28e24a6e44f5cbf912c66dde6ab7e07d1059a205
React: c2830fa483b0334bda284e46a8579ebbe0c5447e
React-callinvoker: 4aecde929540c26b841a4493f70ebf6016691eb8
React-Core: 1e3c04337857fa7fb7559f73f6f29a2a83a84b9c
React-CoreModules: 9fac2d31803c0ed03e4ddaa17f1481714f8633a5
React-cxxreact: c72a7a8066fc4323ea85a3137de50c8a10a69794
React-Core: 65374ea054f3f00eaa3c8bb5e989cb1ba8128844
React-CoreModules: f53e0674e1747fa41c83bc970e82add97b14ad87
React-cxxreact: bb77e88b645c5378ecd0c30c94f965a8294001d8
React-debug: 7e346b6eeacd2ee1118a0ee7d39f613b428b4be8
React-defaultsnativemodule: e40e760aa97a7183d5f5a8174e44026673c4b995
React-domnativemodule: 9fef73afd600e7c7d7f540d82532a113830bbdda
React-Fabric: dcd7ec3ea4da022b6c3f025e2567c9860ff1f760
React-FabricComponents: 7e67af984cab1d6d1c02aae4a62933abc1baa5d3
React-FabricImage: 77ca01a0a2bca3e1d39967220d7af7e3de033c9f
React-defaultsnativemodule: 4f1e9236c048fce31ebaf2c9c59ad7e76fb971a1
React-domnativemodule: 0d0e04cd8a68f3984b7b15aada7ff531dfc3c3bd
React-Fabric: fa636eabfe3c8a3af3a9bface586956e90619ebf
React-FabricComponents: 52382f668a934df9cef21a7893beffbe0e2b2f5e
React-FabricImage: 69b745c0231d9360180f5e411370c6fb0c3cb546
React-featureflags: 4c45b3c06f9a124d2598aff495bfc59470f40597
React-featureflagsnativemodule: d37e4fe27bd4f541d6d46f05e899345018067314
React-graphics: a2e6209991a191c94405a234460e05291fa986b9
React-idlecallbacksnativemodule: fa07e0af59ec6c950b2156b14c73c7fce4d0a663
React-ImageManager: 17772f78d93539a1a10901b5f537031772fa930c
React-featureflagsnativemodule: 110c225191b3bca92694d36303385c2c299c12e5
React-graphics: eb61d404819486a2d9335c043a967a0c4b8ca743
React-idlecallbacksnativemodule: ca6930a17eaae01591610c87b19dbd90515f54a1
React-ImageManager: 6652c4cc3de260b5269d58277de383cacd53a234
React-jsc: 4d3352be620f3fe2272238298aaccc9323b01824
React-jserrorhandler: 62af5111f6444688182a5850d4b584cbc0c5d6a8
React-jsi: 490deef195fd3f01d57dc89dda8233a84bd54b83
React-jsiexecutor: 13bcb5e11822b2a6b69dbb175a24a39e24a02312
React-jsinspector: 6961a23d4c11b72f3cbeb5083b0b18cc22bc48a1
React-jsitracing: dab78a74a581f63320604c9de4ab9039209e0501
React-logger: d79b704bf215af194f5213a6b7deec50ba8e6a9b
React-Mapbuffer: 42c779748af341935a63ad8831723b8cb1e97830
React-microtasksnativemodule: 744f7e26200ea3976fef8453101cefcc08756008
react-native-biometrics: 352e5a794bfffc46a0c86725ea7dc62deb085bdc
react-native-cloud-storage: 4c68bc6025c3624164461e15231efb28576f78a8
react-native-date-picker: 5637f417bb0c1981bc9d483324d5eb5929a1651c
react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06
react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac
react-native-nfc-manager: 5213321cf6c18d879c8092c0bf56806b771ec5ac
react-native-orientation-locker: 5819fd23ca89cbac0d736fb4314745f62716d517
react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846
react-native-safe-area-context: 849d7df29ecb2a7155c769c0b76849ba952c2aa3
react-native-tracking-transparency: 25ff1ff866e338c137c818bdec20526bb05ffcc1
React-jserrorhandler: 552c5fcd2ee64307c568734b965ea082e1be25cf
React-jsi: b187c826e5bda25afb36ede4c54c146cd50c9d6c
React-jsiexecutor: ac8478b6c5f53bcf411a66bf4461e923dafeb0bd
React-jsinspector: a82cfe0794b831d6e36cf0c8c07da56a0aaa1282
React-jsitracing: e512a1023a25de831b51be1c773caa6036125a44
React-logger: 80d87daf2f98bf95ab668b79062c1e0c3f0c2f8a
React-Mapbuffer: b2642edd9be75d51ead8cda109c986665eae09cf
React-microtasksnativemodule: 7ebf131e1792a668004d2719a36da0ff8d19c43c
react-native-biometrics: 43ed5b828646a7862dbc7945556446be00798e7d
react-native-cloud-storage: 74d1f1456d714e0fca6d10c7ab6fe9a52ba203b6
react-native-date-picker: 2dd40e0ab5be141f96839998e944fa2f23f227af
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
react-native-nfc-manager: a280ef94cd4871a471b052f0dc70381cf1223049
react-native-orientation-locker: cc6f357b289a2e0dd2210fea0c52cb8e0727fdaa
react-native-randombytes: 3c8f3e89d12487fd03a2f966c288d495415fc116
react-native-safe-area-context: 3e33e7c43c8b74dba436a5a32651cb8d7064c740
react-native-tracking-transparency: 15eb319f2b982070eb9831582af27d87badfa624
React-nativeconfig: 31072ab0146e643594f6959c7f970a04b6c9ddd0
React-NativeModulesApple: 5df767d9a2197ac25f4d8dd2d4ae1af3624022e2
React-NativeModulesApple: 4ffcab4cdf34002540799bffbedd6466e8023c3a
React-perflogger: 59e1a3182dca2cee7b9f1f7aab204018d46d1914
React-performancetimeline: 3d70a278cc3344def506e97aff3640e658656110
React-performancetimeline: 2bf8625ff44f482cba84e48e4ab21dee405d68cd
React-RCTActionSheet: d80e68d3baa163e4012a47c1f42ddd8bcd9672cc
React-RCTAnimation: bde981f6bd7f8493696564da9b3bd05721d3b3cc
React-RCTAppDelegate: bc9c02d6dd4d162e3e1850283aba81bd246fc688
React-RCTBlob: e492d54533e61a81f2601494a6f393b3e15e33b9
React-RCTFabric: 4556aa70bd55b48d793cfb87e80d687c164298e2
React-RCTImage: 90448d2882464af6015ed57c98f463f8748be465
React-RCTLinking: 1bd95d0a704c271d21d758e0f0388cced768d77d
React-RCTNetwork: 218af6e63eb9b47935cc5a775b7a1396cf10ff91
React-RCTSettings: e10b8e42b0fce8a70fbf169de32a2ae03243ef6b
React-RCTText: e7bf9f4997a1a0b45c052d4ad9a0fe653061cf29
React-RCTVibration: 5b70b7f11e48d1c57e0d4832c2097478adbabe93
React-RCTAnimation: 051f0781709c5ed80ba8aa2b421dfb1d72a03162
React-RCTAppDelegate: 99345256dcceddcacab539ff8f56635de6a2f551
React-RCTBlob: e949797c162421e363f93bfd8b546b7e632ba847
React-RCTFabric: 396093d9aeee4bd3a6021ec6df8ed012f78763ef
React-RCTImage: b73149c0cd54b641dba2d6250aaf168fee784d9f
React-RCTLinking: 23e519712285427e50372fbc6e0265d422abf462
React-RCTNetwork: a5d06d122588031989115f293654b13353753630
React-RCTSettings: 87d03b5d94e6eadd1e8c1d16a62f790751aafb55
React-RCTText: 75e9dd39684f4bcd1836134ac2348efaca7437b3
React-RCTVibration: 033c161fe875e6fa096d0d9733c2e2501682e3d4
React-rendererconsistency: 35cef4bc4724194c544b6e5e2bd9b3f7aff52082
React-rendererdebug: 9b1a6a2d4f8086a438f75f28350ccba16b7b706a
React-rendererdebug: 4e801e9f8d16d21877565dca2845a2e56202b8c6
React-rncore: 2c7c94d6e92db0850549223eb2fa8272e0942ac2
React-RuntimeApple: 22397aca29a0c9be681db02c68416e508a381ef1
React-RuntimeCore: a6d413611876d8180a5943b80cba3cefdf95ad5f
React-RuntimeApple: 0f661760cfcfa5d9464f7e05506874643e88fc2d
React-RuntimeCore: 1d0fcc0eb13807818e79ccaf48915596f0f5f0e6
React-runtimeexecutor: ea90d8e3a9e0f4326939858dafc6ab17c031a5d3
React-runtimescheduler: e041df0539ad8a8a370e3507c39a9ab0571bb848
React-utils: 768a7eb396b7df37aa19389201652eac613490cd
ReactCodegen: c53f8a0fa088739ee9929820feec1508043c7e6c
ReactCommon: 03d2d48fcd1329fe3bc4e428a78a0181b68068c2
RNCAsyncStorage: 03861ec2e1e46b20e51963c62c51dc288beb7c43
RNCClipboard: 60fed4b71560d7bfe40e9d35dea9762b024da86d
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: e705387b01bba53f4643bdff381ee08c7b9679a1
RNKeychain: bfe3d12bf4620fe488771c414530bf16e88f3678
RNLocalize: 06991b9c31e7a898a9fa6ddb204ce0f53a967248
RNReactNativeHapticFeedback: cba92e59f56506f6058d261dc85986012b2c5032
RNScreens: 7cdbd2d97472f2838cee0d53171a89e7e0c30991
RNSVG: 669ed128ab9005090c612a0d627dbecb6ab5c76f
RNZipArchive: 6d736ee4e286dbbd9d81206b7a4da355596ca04a
segment-analytics-react-native: d57ed4971cbb995706babf29215ebdbf242ecdab
React-runtimescheduler: 6b33edee8c830c7926670df4232d51f4f6a82795
React-utils: 7198bd077f07ce8f9263c05bf610da6e251058ad
ReactCodegen: a2d336e0bec3d2f45475df55e7a02cc4e4c19623
ReactCommon: b02a50498cb1071cd793044ddbd5d2b5f4db0a34
RNCAsyncStorage: af7b591318005069c3795076addc83a4dd5c0a2e
RNCClipboard: 4abb037e8fe3b98a952564c9e0474f91c492df6d
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
RNGestureHandler: 4fb54320f931ff08e4d68754435728650cba91fd
RNGoogleSignin: ba93c1137f8d5cebdd39b04f493fd212ddf5ecd6
RNKeychain: bbe2f6d5cc008920324acb49ef86ccc03d3b38e4
RNLocalize: 15463c4d79c7da45230064b4adcf5e9bb984667e
RNReactNativeHapticFeedback: e19b9b2e2ecf5593de8c4ef1496e1e31ae227514
RNScreens: 739ab5579c95cc477744d1c68473395a909a3062
RNSVG: 46769c92d1609e617dbf9326ad8a0cff912d0982
RNZipArchive: 29236989a51e92d44e0a5d4c02555426e4e4726c
segment-analytics-react-native: 6f98edf18246782ee7428c5380c6519a3d2acf5e
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
sovran-react-native: eec37f82e4429f0e3661f46aaf4fcd85d1b54f60
sovran-react-native: a3ad3f8ff90c2002b2aa9790001a78b0b0a38594
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
SwiftQRScanner: e85a25f9b843e9231dab89a96e441472fe54a724
SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb

View File

@@ -32,6 +32,7 @@
"@react-native-async-storage/async-storage": "^2.1.1",
"@react-native-clipboard/clipboard": "1.13.2",
"@react-native-community/netinfo": "^11.3.3",
"@react-native-google-signin/google-signin": "^13.1.0",
"@react-navigation/elements": "^2.2.5",
"@react-navigation/native": "^7.0.14",
"@react-navigation/native-stack": "^7.2.0",

View File

@@ -13,7 +13,6 @@ import HomeNavBar from './components/HomeNavBar';
import AccountRecoveryChoiceScreen from './screens/AccountFlow/AccountRecoveryChoiceScreen';
import AccountRecoveryScreen from './screens/AccountFlow/AccountRecoveryScreen';
import AccountVerifiedSuccessScreen from './screens/AccountFlow/AccountVerifiedSuccessScreen';
import RecoverWithCloudScreen from './screens/AccountFlow/RecoverWithCloud';
import RecoverWithPhraseScreen from './screens/AccountFlow/RecoverWithPhraseScreen';
import SaveRecoveryPhraseScreen from './screens/AccountFlow/SaveRecoveryPhraseScreen';
import DisclaimerScreen from './screens/DisclaimerScreen';
@@ -217,12 +216,6 @@ const AppNavigation = createNativeStackNavigator({
headerBackTitle: 'close',
},
},
RecoverWithCloud: {
screen: RecoverWithCloudScreen,
options: {
headerShown: false,
},
},
AccountVerifiedSuccess: {
screen: AccountVerifiedSuccessScreen,
options: {

View File

@@ -15,9 +15,11 @@ const BackupDocumentationLink: React.FC<
BackupDocumentationLinkProps
> = ({}) => {
if (Platform.OS === 'ios') {
<StyledAnchor unstyled href="https://support.apple.com/en-us/102651">
iCloud data
</StyledAnchor>;
return (
<StyledAnchor unstyled href="https://support.apple.com/en-us/102651">
iCloud data
</StyledAnchor>
);
}
return (
<StyledAnchor

View File

@@ -21,9 +21,9 @@ export const useAppUpdates = (): [boolean, () => void, boolean] => {
bodyText:
"We've improved performance, fixed bugs, and added new features. Update now to install the latest version of Self.",
buttonText: 'Update and restart',
onButtonPress: () => {
onButtonPress: async () => {
if (newVersionUrl !== null) {
Linking.openURL(
await Linking.openURL(
newVersionUrl, // TODO or use: `Platform.OS === 'ios' ? appStoreUrl : playStoreUrl`
);
}

25
app/src/hooks/useModal.ts Normal file
View File

@@ -0,0 +1,25 @@
import { useCallback } from 'react';
import { useNavigation } from '@react-navigation/native';
import { ModalParams } from '../screens/Settings/ModalScreen';
export const useModal = (params: ModalParams) => {
const navigation = useNavigation();
const showModal = useCallback(() => {
navigation.navigate('Modal', params);
}, [navigation, params]);
const dismissModal = useCallback(() => {
const routes = navigation.getState()?.routes;
if (routes?.at(routes.length - 1)?.name === 'Modal') {
navigation.goBack();
}
}, [navigation, params]);
return {
showModal,
dismissModal,
};
};

View File

@@ -23,11 +23,12 @@ const AccountRecoveryChoiceScreen: React.FC<
> = ({}) => {
const { restoreAccountFromPrivateKey } = useAuth();
const [restoring, setRestoring] = useState(false);
const { cloudBackupEnabled, toggleCloudBackupEnabled } = useSettingStore();
const { cloudBackupEnabled, toggleCloudBackupEnabled, biometricsAvailable } =
useSettingStore();
const { download } = useBackupPrivateKey();
const onRestoreFromCloudNext = useHapticNavigation('AccountVerifiedSuccess');
const onEnterRecoveryPress = useHapticNavigation('SaveRecoveryPhrase');
const onEnterRecoveryPress = useHapticNavigation('RecoverWithPhrase');
const onRestoreFromCloudPress = useCallback(async () => {
setRestoring(true);
@@ -63,15 +64,20 @@ const AccountRecoveryChoiceScreen: React.FC<
<Description>
By continuing, you certify that this passport belongs to you and is
not stolen or forged.
{biometricsAvailable && (
<>
Your device doesn't support biometrics or is disabled for apps
and is required for cloud storage.
</>
)}
</Description>
<YStack gap="$2.5" width="100%" pt="$6">
<PrimaryButton
onPress={onRestoreFromCloudPress}
// disabled={restoring}
disabled
disabled={restoring || !biometricsAvailable}
>
Restore from {STORAGE_NAME} (soon)
Restore from {STORAGE_NAME}
</PrimaryButton>
<XStack gap={64} ai="center" justifyContent="space-between">
<Separator flexGrow={1} />

View File

@@ -1,68 +0,0 @@
import React, { useCallback } from 'react';
import { useNavigation } from '@react-navigation/native';
import { YStack } from 'tamagui';
import BackupDocumentationLink from '../../components/BackupDocumentationLink';
import { PrimaryButton } from '../../components/buttons/PrimaryButton';
import { Caption } from '../../components/typography/Caption';
import Description from '../../components/typography/Description';
import { Title } from '../../components/typography/Title';
import Cloud from '../../images/icons/logo_cloud_backup.svg';
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
import { useAuth } from '../../stores/authProvider';
import { useSettingStore } from '../../stores/settingStore';
import { STORAGE_NAME, useBackupPrivateKey } from '../../utils/cloudBackup';
import { black, white } from '../../utils/colors';
interface RecoverWithCloudScreenProps {}
const RecoverWithCloudScreen: React.FC<RecoverWithCloudScreenProps> = ({}) => {
const navigation = useNavigation();
const { restoreAccountFromPrivateKey } = useAuth();
const { cloudBackupEnabled, toggleCloudBackupEnabled } = useSettingStore();
const { download } = useBackupPrivateKey();
const restoreBackup = useCallback(async () => {
const restoredPrivKey = await download();
try {
await restoreAccountFromPrivateKey(restoredPrivKey);
if (!cloudBackupEnabled) {
toggleCloudBackupEnabled();
}
navigation.navigate('AccountVerifiedSuccess');
} catch (e) {
console.error(e);
throw new Error('Something wrong happened during cloud recovery');
}
}, [cloudBackupEnabled, download, restoreAccountFromPrivateKey]);
return (
<ExpandableBottomLayout.Layout backgroundColor={black}>
<ExpandableBottomLayout.TopSection backgroundColor={black}>
<Cloud height={200} width={140} color={white} />
</ExpandableBottomLayout.TopSection>
<ExpandableBottomLayout.BottomSection
flexGrow={1}
backgroundColor={white}
>
<YStack gap="$10">
<YStack gap="$2.5" alignItems="center">
<Title>Restore your Self Account</Title>
<Description>
Your account will safely downloaded and restored from{' '}
{STORAGE_NAME}.
</Description>
<Caption>
Learn more about <BackupDocumentationLink />
</Caption>
</YStack>
<PrimaryButton onPress={restoreBackup}>
Restore from {STORAGE_NAME}
</PrimaryButton>
</YStack>
</ExpandableBottomLayout.BottomSection>
</ExpandableBottomLayout.Layout>
);
};
export default RecoverWithCloudScreen;

View File

@@ -74,7 +74,7 @@ const SaveRecoveryPhraseScreen: React.FC<
You can reveal your recovery phrase in settings.
</Caption>
<PrimaryButton onPress={onCloudBackupPress}>
Enable {STORAGE_NAME}
Enable {STORAGE_NAME} backups
</PrimaryButton>
<SecondaryButton onPress={onSkipPress}>
{userHasSeenMnemonic ? 'Continue' : 'Skip making a backup'}

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { StaticScreenProps, useNavigation } from '@react-navigation/native';
import { YStack } from 'tamagui';
@@ -10,6 +10,7 @@ import { SecondaryButton } from '../../components/buttons/SecondaryButton';
import { Caption } from '../../components/typography/Caption';
import Description from '../../components/typography/Description';
import { Title } from '../../components/typography/Title';
import { useModal } from '../../hooks/useModal';
import Cloud from '../../images/icons/logo_cloud_backup.svg';
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
import { useAuth } from '../../stores/authProvider';
@@ -20,7 +21,7 @@ import { black, white } from '../../utils/colors';
interface CloudBackupScreenProps
extends StaticScreenProps<
| {
nextScreen: keyof RootStackParamList;
nextScreen?: Omit<'CloudBackupSettings', keyof RootStackParamList>;
}
| undefined
> {}
@@ -29,23 +30,62 @@ const CloudBackupScreen: React.FC<CloudBackupScreenProps> = ({
route: { params },
}) => {
const navigation = useNavigation();
const { getOrCreatePrivateKey } = useAuth();
const { cloudBackupEnabled, toggleCloudBackupEnabled } = useSettingStore();
const { getOrCreatePrivateKey, loginWithBiometrics } = useAuth();
const { cloudBackupEnabled, toggleCloudBackupEnabled, biometricsAvailable } =
useSettingStore();
const { upload, disableBackup } = useBackupPrivateKey();
const [pending, setPending] = useState(false);
const toggleBackup = useCallback(async () => {
const privKey = await getOrCreatePrivateKey();
if (!privKey) {
const { showModal } = useModal(
useMemo(
() => ({
titleText: 'Disable cloud backups',
bodyText:
'Are you sure you want to disable cloud backups, you may lose your recovery phrase.',
buttonText: 'I understand the risks',
onButtonPress: async () => {
try {
await loginWithBiometrics();
await disableBackup();
toggleCloudBackupEnabled();
} finally {
setPending(false);
}
},
onModalDismiss: () => {
setPending(false);
},
}),
[loginWithBiometrics, disableBackup, toggleCloudBackupEnabled],
),
);
const enableCloudBackups = useCallback(async () => {
if (cloudBackupEnabled) {
return;
}
if (cloudBackupEnabled) {
await disableBackup();
} else {
await upload(privKey.data);
setPending(true);
const privKey = await getOrCreatePrivateKey();
if (!privKey) {
setPending(false);
return;
}
await upload(privKey.data);
toggleCloudBackupEnabled();
}, [cloudBackupEnabled, upload, getOrCreatePrivateKey]);
setPending(false);
}, [
cloudBackupEnabled,
getOrCreatePrivateKey,
upload,
toggleCloudBackupEnabled,
]);
const disableCloudBackups = useCallback(() => {
setPending(true);
showModal();
}, [showModal]);
return (
<ExpandableBottomLayout.Layout backgroundColor={black}>
@@ -68,17 +108,34 @@ const CloudBackupScreen: React.FC<CloudBackupScreenProps> = ({
: `Your account will be end-to-end encrypted backed up to ${STORAGE_NAME} so you can easily restore it if you ever get a new phone.`}
</Description>
<Caption>
Learn more about <BackupDocumentationLink />
{biometricsAvailable ? (
<>
Learn more about <BackupDocumentationLink />
</>
) : (
<>
Your device doesn't support biometrics or is disabled for apps
and is required for cloud storage.
</>
)}
</Caption>
<YStack gap="$2.5" width="100%" pt="$6">
{cloudBackupEnabled ? (
<SecondaryButton onPress={toggleBackup}>
Disable {STORAGE_NAME}
<SecondaryButton
onPress={disableCloudBackups}
disabled={pending || !biometricsAvailable}
>
{pending ? 'Disabling' : 'Disable'} {STORAGE_NAME} backups
{pending ? '' : ''}
</SecondaryButton>
) : (
<PrimaryButton onPress={toggleBackup}>
Enable {STORAGE_NAME}
<PrimaryButton
onPress={enableCloudBackups}
disabled={pending || !biometricsAvailable}
>
{pending ? 'Enabling' : 'Enable'} {STORAGE_NAME} backups
{pending ? '' : ''}
</PrimaryButton>
)}

View File

@@ -1,12 +1,11 @@
import React from 'react';
import React, { useCallback, useState } from 'react';
import { StaticScreenProps } from '@react-navigation/native';
import { StaticScreenProps, useNavigation } from '@react-navigation/native';
import { View, XStack, YStack, styled } from 'tamagui';
import { PrimaryButton } from '../../components/buttons/PrimaryButton';
import Description from '../../components/typography/Description';
import { Title } from '../../components/typography/Title';
import useHapticNavigation from '../../hooks/useHapticNavigation';
import ModalClose from '../../images/icons/modal_close.svg';
import LogoInversed from '../../images/logo_inversed.svg';
import { white } from '../../utils/colors';
@@ -23,25 +22,28 @@ const ModalBackDrop = styled(View, {
height: '100%',
});
const ModalDescription = styled(Description, {
textAlign: 'left',
});
export interface ModalParams extends Record<string, any> {
titleText: string;
bodyText: string;
buttonText: string;
onButtonPress: (() => Promise<void>) | (() => void);
onModalDismiss: () => void;
}
interface ModalScreenProps
extends StaticScreenProps<{
titleText: string;
bodyText: string;
buttonText: string;
onButtonPress: () => void;
onModalDismiss: () => void;
}> {}
interface ModalScreenProps extends StaticScreenProps<ModalParams> {}
const ModalScreen: React.FC<ModalScreenProps> = ({ route: { params } }) => {
const navigateBack = useHapticNavigation('Home', { action: 'cancel' });
const onButtonPressed = () => {
params?.onButtonPress();
navigateBack();
};
const navigation = useNavigation();
const [pending, setPending] = useState(false);
const onButtonPressed = useCallback(async () => {
setPending(true);
try {
await params?.onButtonPress();
navigation.goBack();
} finally {
setPending(false);
}
}, []);
return (
<ModalBackDrop>
@@ -51,16 +53,18 @@ const ModalScreen: React.FC<ModalScreenProps> = ({ route: { params } }) => {
<LogoInversed />
<ModalClose
onPress={() => {
navigateBack();
navigation.goBack();
params?.onModalDismiss();
}}
/>
</XStack>
<YStack gap={20}>
<Title textAlign="left">{params?.titleText}</Title>
<ModalDescription>{params?.bodyText}</ModalDescription>
<Description style={{ textAlign: 'left' }}>
{params?.bodyText}
</Description>
</YStack>
<PrimaryButton onPress={onButtonPressed}>
<PrimaryButton onPress={onButtonPressed} disabled={pending}>
{params?.buttonText}
</PrimaryButton>
</YStack>

View File

@@ -52,7 +52,7 @@ const InfoRow: React.FC<{
interface PassportDataInfoScreenProps {}
const PassportDataInfoScreen: React.FC<PassportDataInfoScreenProps> = ({}) => {
const { getMetadata } = usePassport();
const { getData } = usePassport();
const [metadata, setMetadata] = useState<PassportMetadata | null>(null);
const loadData = useCallback(async () => {
@@ -60,13 +60,15 @@ const PassportDataInfoScreen: React.FC<PassportDataInfoScreenProps> = ({}) => {
return;
}
const result = await getMetadata();
const result = await getData();
if (!result || !result.data) {
// maybe handle error instead
return;
}
setMetadata(result.data);
}, [metadata, getMetadata]);
setMetadata(result.data.passportMetadata!);
}, [metadata, getData]);
useFocusEffect(() => {
loadData();

View File

@@ -19,7 +19,7 @@ import {
telegramUrl,
} from '../consts/links';
import Github from '../images/icons/github.svg';
// import Cloud from '../images/icons/settings_cloud_backup.svg';
import Cloud from '../images/icons/settings_cloud_backup.svg';
import Data from '../images/icons/settings_data.svg';
import Feedback from '../images/icons/settings_feedback.svg';
import Lock from '../images/icons/settings_lock.svg';
@@ -50,7 +50,7 @@ const storeURL = Platform.OS === 'ios' ? appStoreUrl : playStoreUrl;
const routes = [
[Data, 'View passport info', 'PassportDataInfo'],
[Lock, 'Reveal recovery phrase', 'ShowRecoveryPhrase'],
// [Cloud, 'Cloud backup', 'CloudBackupSettings'],
[Cloud, 'Cloud backup', 'CloudBackupSettings'],
[Feedback, 'Send feeback', 'email_feedback'],
[ShareIcon, 'Share Self app', 'share'],
] as [React.FC<SvgProps>, string, RouteOption][];
@@ -143,7 +143,6 @@ ${deviceInfo.map(([k, v]) => `${k}=${v}`).join('; ')}
break;
default:
// @ts-expect-error - weird typing?
navigation.navigate(menuRoute);
break;
}

View File

@@ -1,18 +1,33 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import LottieView from 'lottie-react-native';
import splashAnimation from '../assets/animations/splash.json';
import { useAuth } from '../stores/authProvider';
import { useSettingStore } from '../stores/settingStore';
import useUserStore from '../stores/userStore';
import { black } from '../utils/colors';
import { impactLight } from '../utils/haptic';
const SplashScreen: React.FC = ({}) => {
const navigation = useNavigation();
const { createSigningKeyPair } = useAuth();
const { setBiometricsAvailable } = useSettingStore();
const { userLoaded, passportData } = useUserStore();
useEffect(() => {
createSigningKeyPair()
.then(setBiometricsAvailable)
.catch(err => {
console.warn(
'Something ELSE and totally unexpected went wrong during keypair creation',
err,
);
});
}, []);
const redirect = useCallback(() => {
if (userLoaded && passportData) {
navigation.navigate('Home');

View File

@@ -21,24 +21,16 @@ const _getSecurely = async function <T>(
return null;
}
let result: Awaited<ReturnType<typeof biometrics.createSignature>>;
const args = {
const result = await biometrics.createSignature({
payload: dataString,
promptMessage: 'Allow access to account private key',
};
try {
result = await biometrics.createSignature(args);
} catch (e) {
console.log(
'No enrolled public key. Creating a public key from biometrics',
);
await biometrics.createKeys();
result = await biometrics.createSignature(args);
}
// @ts-expect-error
allowDeviceCredentials: true,
});
const { error, success, signature } = result;
if (error) {
// handle error
console.log(result, error, success, signature);
throw error;
}
if (!success) {
@@ -52,6 +44,31 @@ const _getSecurely = async function <T>(
};
};
async function createSigningKeyPair(): Promise<boolean> {
const { available } = await biometrics.isSensorAvailable();
if (!available) {
return false;
}
if ((await biometrics.biometricKeysExist()).keysExist) {
return true;
}
console.log('No enrolled public key. Creating a public key from biometrics');
try {
await biometrics.createKeys();
return true;
} catch (e) {
if (available) {
console.error(
"User has biometrics but somehow it wasn't able to create keys",
);
return false;
} else {
throw e;
}
}
}
async function loadSecret() {
const secretCreds = await Keychain.getGenericPassword({ service: 'secret' });
return secretCreds === false ? false : secretCreds.password;
@@ -100,6 +117,7 @@ interface IAuthContext {
restoreAccountFromPrivateKey: (
privKey: string,
) => Promise<SignedPayload<string> | null>;
createSigningKeyPair: () => Promise<boolean>;
}
export const AuthContext = createContext<IAuthContext>({
isAuthenticated: false,
@@ -109,6 +127,7 @@ export const AuthContext = createContext<IAuthContext>({
getOrCreatePrivateKey: () => Promise.resolve(null),
restoreAccountFromMnemonic: () => Promise.resolve(null),
restoreAccountFromPrivateKey: () => Promise.resolve(null),
createSigningKeyPair: () => Promise.resolve(false),
});
export const AuthProvider = ({
@@ -185,6 +204,7 @@ export const AuthProvider = ({
getOrCreatePrivateKey,
restoreAccountFromMnemonic,
restoreAccountFromPrivateKey,
createSigningKeyPair,
_getSecurely,
}),
[isAuthenticated, isAuthenticatingPromise, loginWithBiometrics],

View File

@@ -5,6 +5,8 @@ import { createJSONStorage, persist } from 'zustand/middleware';
interface SettingsState {
hasPrivacyNoteBeenDismissed: boolean;
dismissPrivacyNote: () => void;
biometricsAvailable: boolean;
setBiometricsAvailable: (biometricsAvailable: boolean) => void;
cloudBackupEnabled: boolean;
toggleCloudBackupEnabled: () => void;
}
@@ -18,6 +20,12 @@ export const useSettingStore = create<SettingsState>()(
hasPrivacyNoteBeenDismissed: false,
dismissPrivacyNote: () => set({ hasPrivacyNoteBeenDismissed: true }),
biometricsAvailable: false,
setBiometricsAvailable: biometricsAvailable =>
set({
biometricsAvailable,
}),
cloudBackupEnabled: false,
toggleCloudBackupEnabled: () =>
set(oldState => ({

View File

@@ -0,0 +1,34 @@
import {
GoogleSignin,
isErrorWithCode,
statusCodes,
} from '@react-native-google-signin/google-signin';
GoogleSignin.configure({
scopes: ['https://www.googleapis.com/auth/drive.appdata'],
});
export async function googleSignIn() {
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;
} 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;
}
}

View File

@@ -1,90 +1,108 @@
import { useMemo } from 'react';
import { NativeModules, Platform } from 'react-native';
import { CloudStorage, CloudStorageScope } from 'react-native-cloud-storage';
import RNFS from 'react-native-fs';
import { Platform } from 'react-native';
import {
CloudStorage,
CloudStorageProvider,
CloudStorageScope,
} from 'react-native-cloud-storage';
// Note: also defined in app/android/app/src/main/res/xml/backup_rules.xml
const ENCRYPTED_FILE_PATH =
RNFS.DocumentDirectoryPath + '/encrypted-private-key';
import { name } from '../../../package.json';
import { googleSignIn } from './google';
export const STORAGE_NAME =
Platform.OS === 'ios' ? 'iCloud Backup' : 'Android Backup';
const FOLDER = `/${name}`;
const ENCRYPTED_FILE_PATH = `/${FOLDER}/encrypted-private-key`;
CloudStorage.setProviderOptions({ scope: CloudStorageScope.AppData });
export const useBackupPrivateKey =
Platform.OS === 'ios'
? useICloudBackupPrivateKey
: useAndroidBackupPrivateKey;
export const STORAGE_NAME = Platform.OS === 'ios' ? 'iCloud' : 'Google Drive';
/// ANDROID
function useAndroidBackupPrivateKey() {
/**
* For some reason google drive api can be very ... brittle and abort randomly (network conditions)
* so retry a couple times for good measure.
*
* Filter the error message by checking if `abort` is included didnt help as the error can be `path not found`
* maybe some race conditions on the drive side
*/
async function withRetries<T>(
promiseBuilder: () => Promise<T>,
retries = 10,
): Promise<T> {
let latestError: Error;
for (let i = 0; i < retries; i++) {
try {
return await promiseBuilder();
} catch (e) {
retries++;
latestError = e as Error;
if (retries < i - 1) {
console.info('retry #', i);
await new Promise(resolve => setTimeout(resolve, 200 * i));
}
}
}
throw new Error(
`retry count exhausted (${retries}), original error ${latestError!}`,
);
}
export function useBackupPrivateKey() {
return useMemo(
() => ({
upload: (privateKey: string) => backupWithAndroidBackup(privateKey),
download: () => downloadFromAndroidBackup(),
disableBackup: () => disableBackupToAndroidBackup,
upload,
download,
disableBackup,
}),
[],
);
}
async function backupWithAndroidBackup(privateKey: string) {
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(privateKey: string) {
if (!privateKey) {
throw new Error(
'Private key not set yet. Did the user see the recovery phrase?',
);
}
const { BackupModule } = NativeModules;
await RNFS.write(ENCRYPTED_FILE_PATH, privateKey);
await BackupModule.backupNow();
}
async function downloadFromAndroidBackup() {
const { BackupModule } = NativeModules;
await BackupModule.restoreNow();
const privateKey = await RNFS.readFile(ENCRYPTED_FILE_PATH);
return privateKey;
}
async function disableBackupToAndroidBackup() {
const { BackupModule } = NativeModules;
await RNFS.unlink(ENCRYPTED_FILE_PATH);
await BackupModule.backupNow();
}
/// IOS
function useICloudBackupPrivateKey() {
return useMemo(
() => ({
upload: (privateKey: string) => backupWithICloud(privateKey),
download: () => downloadFromICloud(),
disableBackup: () => disableBackupToICloud,
}),
[],
);
}
async function backupWithICloud(privateKey: string) {
if (!privateKey) {
throw new Error(
'Private key 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 CloudStorage.writeFile(
ENCRYPTED_FILE_PATH,
privateKey,
CloudStorageScope.AppData,
await withRetries(() =>
CloudStorage.writeFile(ENCRYPTED_FILE_PATH, privateKey),
);
}
async function downloadFromICloud() {
const privateKey = await CloudStorage.readFile(
ENCRYPTED_FILE_PATH,
CloudStorageScope.AppData,
async function download() {
await addAccessTokenForGoogleDrive();
if (await CloudStorage.exists(ENCRYPTED_FILE_PATH)) {
const privateKey = await withRetries(() =>
CloudStorage.readFile(ENCRYPTED_FILE_PATH),
);
return privateKey;
}
throw new Error(
'Couldnt find the encrypted backup, did you back it up previously?',
);
return privateKey;
}
async function disableBackupToICloud() {
await CloudStorage.unlink(ENCRYPTED_FILE_PATH, CloudStorageScope.AppData);
async function disableBackup() {
await addAccessTokenForGoogleDrive();
withRetries(() => CloudStorage.rmdir(FOLDER, { recursive: true }));
}

View File

@@ -2513,6 +2513,23 @@ __metadata:
languageName: node
linkType: hard
"@react-native-google-signin/google-signin@npm:^13.1.0":
version: 13.1.0
resolution: "@react-native-google-signin/google-signin@npm:13.1.0"
peerDependencies:
expo: ">=50.0.0"
react: "*"
react-dom: "*"
react-native: "*"
peerDependenciesMeta:
expo:
optional: true
react-dom:
optional: true
checksum: 10c0/f3e4032e8c5ef36c180f17a5c01cd054c0ea732def808f06871a768204bfdc5a3cdcbc3fa6f1d57aa564b5adb71cf861a3420f5c48d10e2ed9a0e6556adc498b
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"
@@ -12414,6 +12431,7 @@ __metadata:
"@react-native-clipboard/clipboard": "npm:1.13.2"
"@react-native-community/cli": "npm:^14.1.1"
"@react-native-community/netinfo": "npm:^11.3.3"
"@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/metro-config": "npm:0.75.4"
@@ -13175,7 +13193,7 @@ __metadata:
peerDependencies:
react: ">= 17.0.1"
react-native: ">= 0.64.3"
checksum: 10c0/ebb3dfb9a111bf6082866999d31e10f952fcd12bcf2a81210ff9f44a4cb4d25e1b4caac5fdec512b367685e56a37e61de3e6648c2af45b93fcbaf704355dbaa6
checksum: 10c0/0e3f83f59dadc337ab46fa0c59bb2b80eb91d4756f0ac67d7b11e0d97044d58bbaca7ee5c7dcd1e0a389af3f16b0eaca12122d59905013a6f5a2f4423ead49d0
languageName: node
linkType: hard