mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
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:
committed by
GitHub
parent
5f71f9a392
commit
4a615a840f
@@ -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
BIN
app/android/dev-keystore
Normal file
Binary file not shown.
@@ -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/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
25
app/src/hooks/useModal.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
34
app/src/utils/cloudBackup/google.ts
Normal file
34
app/src/utils/cloudBackup/google.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 }));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user