diff --git a/.nvmrc b/.nvmrc index b6a7d89c6..3c032078a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16 +18 diff --git a/README.md b/README.md index f9aebd055..678d973e2 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This notably includes the possiblity to run client-side konnectors, to get your - [iOS only] Install XCode - [Android only] Install Android Studio (or Android SDK) - [Android only] Java 11 +- [Android only] Install NDK (21.4.7075529) and CMake (3.10.2) from Android Studio's SDK Manager - [Android only] Copy the Android's `debug.keystore` from Cozy's password-store into `android/app/debug.keystore` - Run `pass show app-amirale/Certificates/debug.keystore > android/app/debug.keystore` - If you don't have access to Cozy's password-store, just generate a new `debug.keystore` file diff --git a/__mocks__/react-native-performance-mock.ts b/__mocks__/react-native-performance-mock.ts new file mode 100644 index 000000000..9e8a15798 --- /dev/null +++ b/__mocks__/react-native-performance-mock.ts @@ -0,0 +1,10 @@ +import RNPerformance from 'react-native-performance' + +export const mockRNPerformance: jest.Mocked = { + mark: jest.fn(), + measure: jest.fn(), + default: { + mark: jest.fn(), + measure: jest.fn(), + } +} as unknown as jest.Mocked diff --git a/__tests__/jest.config.js b/__tests__/jest.config.js index 0d6117ffd..f94580a2e 100644 --- a/__tests__/jest.config.js +++ b/__tests__/jest.config.js @@ -13,7 +13,7 @@ const config = { '/__tests__/transformer/imageTransformer.js' }, transformIgnorePatterns: [ - 'node_modules/(?!((jest-)?react-native(-.*)?|@react-native(-community)?|p-timeout?|p-wait-for?|@notifee?)|@fengweichong/react-native-gzip?/)' + 'node_modules/(?!((jest-)?react-native(-.*)?|@react-native(-community)?|p-timeout?|p-wait-for?|@notifee?)|@fengweichong/react-native-gzip|@craftzdog/*?/)' ], rootDir: '../' } diff --git a/__tests__/jestSetupFile.js b/__tests__/jestSetupFile.js index d6bd11cdb..8e0939f08 100644 --- a/__tests__/jestSetupFile.js +++ b/__tests__/jestSetupFile.js @@ -13,6 +13,7 @@ import { mockRNBackgroundGeolocation } from '../__mocks__/react-native-backgroun import { mockRNFS } from '../__mocks__/react-native-fs-mock' import { mockRNIAP } from '../__mocks__/react-native-iap-mock' import { mockRNInAppBrowser } from '../__mocks__/react-native-inappbrowser-reborn-mock' +import { mockRNPerformance } from '../__mocks__/react-native-performance-mock' jest.mock('react-native-device-info', () => mockRNDeviceInfo) @@ -29,6 +30,7 @@ jest.mock('@sentry/react-native', () => ({ jest.mock('react-native-fs', () => mockRNFS) jest.mock('react-native-iap', () => mockRNIAP) jest.mock('react-native-inappbrowser-reborn', () => mockRNInAppBrowser) +jest.mock('react-native-performance', () => mockRNPerformance) jest.mock('react-native-ios11-devicecheck', () => ({ getToken: jest.fn() })) @@ -127,3 +129,24 @@ jest.mock('../src/core/tools/env', () => ({ devlog: jest.fn(), shouldDisableAutolock: jest.fn().mockReturnValue(false) })) + +jest.mock('../src/pouchdb/pouchdb', () => ({})) +jest.mock('react-native-quick-websql', () => ({})) + +class mockPouchLink { + constructor() {} +} + +jest.mock('cozy-pouch-link', () => { + return jest.fn().mockImplementation(() => { + return new mockPouchLink() + }) +}) + +jest.mock('react-native-mail', () => ({ + mail: jest.fn() +})) + +jest.mock('/app/domain/search/dataproxy-wrapper', () => ({ + SearchEngine: jest.fn() +})) diff --git a/android/app/build.gradle b/android/app/build.gradle index b713a5146..8c1d3bc3f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -143,6 +143,13 @@ android { prod { } } + + packagingOptions { + pickFirst 'lib/x86/libcrypto.so' + pickFirst 'lib/x86_64/libcrypto.so' + pickFirst 'lib/armeabi-v7a/libcrypto.so' + pickFirst 'lib/arm64-v8a/libcrypto.so' + } } dependencies { diff --git a/android/app/src/main/java/io/cozy/flagship/mobile/MainApplication.java b/android/app/src/main/java/io/cozy/flagship/mobile/MainApplication.java index 321e66509..5a4ca297f 100644 --- a/android/app/src/main/java/io/cozy/flagship/mobile/MainApplication.java +++ b/android/app/src/main/java/io/cozy/flagship/mobile/MainApplication.java @@ -80,15 +80,5 @@ public void onCreate() { WebView.setWebContentsDebuggingEnabled(true); OkHttpClientProvider.setOkHttpClientFactory(new UserAgentClientFactory()); - - try { - Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize"); - field.setAccessible(true); - field.set(null, 50 * 1024 * 1024); // 50MB - } catch (Exception e) { - if (BuildConfig.DEBUG) { - e.printStackTrace(); - } - } } } diff --git a/android/build.gradle b/android/build.gradle index bbddb793a..44bd25207 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -3,13 +3,15 @@ buildscript { ext { buildToolsVersion = "34.0.0" - minSdkVersion = 21 + minSdkVersion = 23 compileSdkVersion = 34 targetSdkVersion = 34 androidXBrowser = "1.4.0" googlePlayServicesLocationVersion = "20.0.0" DocumentScanner_compileSdkVersion = 34 DocumentScanner_targetSdkVersion = 34 + QuickBase64_compileSdkVersion = 34 + QuickBase64_targetSdkVersion = 34 ndkVersion = "25.1.8937393" kotlinVersion = "1.8.0" // To solve a conflict between flipper and react-native-background-geolocation diff --git a/babel.config.js b/babel.config.js index 84c149369..f57a3cf92 100644 --- a/babel.config.js +++ b/babel.config.js @@ -8,6 +8,10 @@ module.exports = { root: ['./'], alias: { '^/(.+)': './src/\\1', + 'pouchdb-collate': '@craftzdog/pouchdb-collate-react-native', + crypto: 'react-native-quick-crypto', + stream: 'readable-stream', + buffer: '@craftzdog/react-native-buffer', '@cozy/minilog': 'cozy-minilog' }, extensions: [ diff --git a/docs/README.md b/docs/README.md index 525f769e1..61e48689a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,8 +8,12 @@ title: Cozy React Native documentation - Table of contents - [How to install cozy-home in local assets](how-to-install-home.md) - [How to retrieve logs in release mode](how-to-retrieve-logs-in-release.md) - [How to debug cozy-bar in local](how-to-debug-cozy-bar.md) +- [How to debug notifications](how-to-debug-notifications.md) +- [How to debug Onboarding partner](how-to-debug-onboarding-partner.md) +- [How to debug Offline mode](how-to-debug-offline-mode.md) ## API documentations - [FlagshipUI API](/src/app/view/FlagshipUI/README.md) - [White Labels management](/white_label/README.md) +- [How to make cozy-apps offline compatible](/how-to-make-cozy-app-offline-compatible.md) diff --git a/docs/how-to-debug-offline-mode.md b/docs/how-to-debug-offline-mode.md new file mode 100644 index 000000000..e628fe2ec --- /dev/null +++ b/docs/how-to-debug-offline-mode.md @@ -0,0 +1,44 @@ +# How to debug Offline mode + +The Flagship app now handle Offline mode, this means that it can be open and run while the device is offline + +This features implies that we can debug the app while using an offline device. This document explains how to do it + +## Android emulator + +On android, in order to set the device offline, use the top drawer menu, open the `Internet` menu and then disable both the Mobile Data and the Wi-Fi entries + +Since ReactNative 0.72, the ReactNative debug link won't work by default if the emulator is offline. This can be fixing by running the following commands from a terminal while the emulator is running: +- `adb root` +- `adb reverse tcp:8081 tcp:8081` +- `adb shell setprop metro.host "localhost"` + +When executed, those commands allow to build, deploy and hot-reload the ReactNative app from an offline emulator. The debug console is also functional + +## iOS simulator + +iOS simulator seems not to provide a comfortable way to debug the app offline. The only way to set the simulator offline is to set the entire computer offline, but this is not an acceptable solution is we are debuging using a locally running cozy-stack as it will still be reachable by the simulator + +## Physical devices + +On physical iOS and Android device, it is possible to debug the app by setting the device offline + +However this will break build, hot-reload and console debugging features + +To mitigate this, it is possible to use Universal Links to extract console logs and DB files from the device + +For console logs, please refer to [How to retrieve logs in release mode](how-to-retrieve-logs-in-release.md) documentation + +For DB files, it is possible to send them by email using the following link: + +- https://links.mycozy.cloud/flagship/senddb or [cozy://senddb](cozy://senddb) + - trigger the OS send email intent pre-filled with DB files and Cozy's support email + +## Reset local PouchDB + +While debugging offline features, it is possible to corrupt the local PouchDB files (i.e. by injecting non valid documents) + +It is possible to reset the local PouchDB files using the following link: + +- https://links.mycozy.cloud/flagship/resetdb or [cozy://resetdb](cozy://resetdb) + - Reset the local PouchDB and restart the app diff --git a/docs/how-to-debug-performances.md b/docs/how-to-debug-performances.md new file mode 100644 index 000000000..c6f0218eb --- /dev/null +++ b/docs/how-to-debug-performances.md @@ -0,0 +1,114 @@ +# How to debug performances + +Since [#1264](https://github.com/cozy/cozy-flagship-app/pull/1264) it is possible to measure the Flagship App's performances. + +This document's goal is to describe how to retrieve measurements, how to read them and how to add new measurements. + +The performance measurements are based on `react-native-performances` plugin and are complementary to CozyClient performance API implementation. More about those two concept can be read here: +- `react-native-performances`: https://github.com/oblador/react-native-performance +- CozyClient Performance API: https://github.com/cozy/cozy-client/blob/master/docs/performances.md + +# Nomenclature + +- `PerformanceAPI`: The API provided by CozyClient for measuring timings in the code base +- `Performance devtools`: Browsers devtools pane dedicated to performance (i.e. [Chrome devtools](https://developer.chrome.com/docs/devtools/performance) and [Firefox profiled](https://profiler.firefox.com/docs/#/)) +- `Flipper performances plugin`: Plugin for Flipper dedicated to performance readings +- `Mark`: A temporal event +- `Measure`: A timing measurement between two temporal events + +# Retrieve measurements + +There are two way to retrieve measurements: + +- In realtime using Flipper +- Asynchronously using Universal Links + +## Flipper plugin + +Flipper allows to read performance measurements in realtime through the `flipper-plugin-performance` plugin: + +- Open Flipper +- Install the plugin `flipper-plugin-performance` +- Start the Flagship app in development mode +- On Flipper, open the `Performance` pane + +![Screenshot of the Flipper plugin](./images/performances-flipper.png) + +## Universal Links + +When it is not possible to use the Flipper plugin, then the Flagship App allows to share performances measurements by email. + +To do this: + +- Open the Flagship app and do the scenario that needs to be measured +- Open one of the following links: + - https://links.mycozy.cloud/flagship/sendperfs + - cozy://sendperfs + +When clicked, the Flagship App will upload the performance measurements for the current session in the user's Cozy and then an email intent will allow to send a link to download them. + +The measurements are uploaded into the `Settings/AALogs/` folder. + +# Read the measurements + +When downloading the measurements that were generated through Universal Link, then it is possible to read them using Chromium devtools. + +To do this: + +- Download the measurements file +- From the `cozy-flagship-app` project, run the following command: + - `yarn perf:convert ` + - This will generate a new file in the same folder as the targeted file, and suffixed with `_converted` +- In a Chromium browser, open the devtools +- Open the `Performance` pane +- In the upper-left of the pane, click on `Load profile...` and selected the `_converted` file + +![Screenshot of the Chromium devtools](./images/performances-devtools.png) + +# Add new measurements + +In order to add new measurements, it is possible to use the API from `/app/domain/performances/measure`. + +This API provides `mark()` and `measure()` methods that are the core concept of doing measurements. + +Those methods have the same behavior as the ones available in CozyClient. Their API and behaviour are described in [the related documentation](https://github.com/cozy/cozy-client/blob/master/docs/performances.md#mark-method) + +## Add new measurements in a method + +In order to measure the duration of a methods or part of a methods: + +```js +import rnperformance from '/app/domain/performances/measure' + +const someMethod = () => { + const markName = rnperformance.mark('someMethod') + + // ... Method code + + rnperformance.measure({ markName }) +} +``` + + +## Add new measurements in a React component + +In order to measure the duration of a React component initialization: + +```js +import rnperformance from '/app/domain/performances/measure' + +const SomeReactComponent = () => { + const [markName] = useState(() => rnperformance.mark('SomeReactComponent')) + + useEffect(() => { + rnperformance.measure({ + markName: markName, + measureName: 'Mount ' + }) + }, [markName]) +} +``` + +## Add new measurements in CozyClient + +See [related documentation](https://github.com/cozy/cozy-client/blob/master/docs/performances.md) \ No newline at end of file diff --git a/docs/how-to-make-cozy-app-offline-compatible.md b/docs/how-to-make-cozy-app-offline-compatible.md new file mode 100644 index 000000000..e787db1e5 --- /dev/null +++ b/docs/how-to-make-cozy-app-offline-compatible.md @@ -0,0 +1,128 @@ +# How to make a cozy-app compatible with Offline mode + +The Flagship app supports Offline mode by allowing cozy-apps to request a local PouchDB when no connection to the remote cozy-stack is available. + +This documentation describes how to make a cozy-app compatible with this Offline mode. + +## Libraries minimum version + +The Offline mode requires the following libraries' minimal versions: +- cozy-client: X.X.X (see [PR 1507](https://github.com/cozy/cozy-client/pull/1507)) // TODO add final version +- cozy-device-helper: 3.1.0 (see [PR 2562](https://github.com/cozy/cozy-libs/pull/2562)) +- cozy-intent: 2.23.0 (see [PR 2562](https://github.com/cozy/cozy-libs/pull/2562)) +- cozy-viewer: X.X.X (see [PR 2581](https://github.com/cozy/cozy-libs/pull/2581)) // TODO add final version + +## Manifest.webapp + +The Flagship app needs to know if a cozy-app supports Offline mode before opening it. This allows to display a dedicated error when trying to open a cozy-app while being offline and when the specified cozy-app does not support this mode. + +To make this possible, cozy-apps should declare their compatibility with Offline mode. + +This can be done in the cozy-app's `manifest.webapp` file, using the `offline_support` attribute: +```json +{ + "name": "Mes Papiers", + "slug": "mespapiers", + //... + "offline_support": true +} +``` + +See [cozy-apps-registry](https://docs.cozy.io/en/cozy-apps-registry/#properties-meaning-reference) documentation for more details. + +## isFlagshipOfflineSupported + +On the other side, cozy-apps may want to verify if the Flagship app supports Offline mode. This check is necessary because we have no control on which version of Flagship app runs on the user's device. + +To make this possible, `cozy-device-helper` exposes the `isFlagshipOfflineSupported()` method that returns `true` when the Flagship app supports Offline mode, otherwise it would return `false`. + +## FlagshipLink + +In order to implement Offline mode, the cozy-app must instantiate `CozyClient` the `FlagshipLink` link. + +When doing so, `CozyClient` will redirect every queries to the Flagship app, the the Flagship app will be responsible to return a result from its local PouchDB or from the remote cozy-stack. + +Note that `FlagshipLink` should be used only when hosted in the Flagship app, otherwise, `CozyStackLink` should be used as before. + +Example of `CozyClient` instantiation with `FlagshipLink` +```js +const shouldUseFlagshipLink = isFlagshipApp() && isFlagshipOfflineSupported() + +// New improvements must be done with CozyClient +const cozyClient = new CozyClient({ + uri: `${window.location.protocol}//${data.cozyDomain}`, + token: data.cozyToken, + links: shouldUseFlagshipLink + ? new FlagshipLink({ webviewIntent: intent }) + : null +}) +``` + +## Queries and Fetch + +When using the `FlagshipLink` every `.query()` calls are redirected to the Flagship app that will execute the given query in either the local PouchDB or the remote cozy-stack + +For now only `.query()` calls are supported, but `fetchJSON()` calls are not. This implies that code that relies on queries will be automatically supported for the Offline mode, but `fetchJSON()` calls will systematically fail when offline. So the cozy-app should ensure that `fetchJSON()` calls are resilient to network errors. + +Don't: +```js +const { data } = await client.stackClient.fetchJSON( + 'GET', + `/registry?versionsChannel=${channel}&filter[type]=konnector&limit=300` +) +``` + +Do: +```js +try { + const { data } = await client.stackClient.fetchJSON( + 'GET', + `/registry?versionsChannel=${channel}&filter[type]=konnector&limit=300` + ) +} catch (error) { + // handle network error here so the app don't crash +} +``` + +Or instead modify the corresponding code to use a `.query()` instead. + +```js +class RegistryCollection extends DocumentCollection { + async get(channel) { + return client.stackClient.fetchJSON( + 'GET', + `/registry?versionsChannel=${channel}&filter[type]=konnector&limit=300` + ) + } +} + +const document = await client.query(Q('io.cozy.registry').getById(channel)) +``` + +When doing so, the query's result will be considered as a DB document and will be stored in the local PouchDB as a virtual document. Then this document will be accessible offline. However to be accessible the corresponding query has to be called at least one time to be callable again when offline. Also returned data won't be synced until next online call. + +## Attributs differences + +Having queries executed from either a local PouchDB or the remote cozy-stack implies some differences in the queries results. + +All queries executing from a local PouchDB may lack attributes related to JSON-API. This is the case for `.attributes` et `.meta` attributes. So cozy-apps should ensure they do not rely on those attributes. + +Don't: +```js +const context = useQuery(contextQuery.definition, contextQuery.options) + +const flags = toFlagNames(context.attributes.features) +``` + +Do: +```js +const context = useQuery(contextQuery.definition, contextQuery.options) + +const flags = toFlagNames(context.features) +``` + +# Implementation examples + +Here are some implementation example for inspiration: +- cozy-home: // TODO add final PR link +- mespapiers: https://github.com/cozy/mespapiers/pull/660 \ No newline at end of file diff --git a/docs/images/performances-devtools.png b/docs/images/performances-devtools.png new file mode 100644 index 000000000..80bb82b15 Binary files /dev/null and b/docs/images/performances-devtools.png differ diff --git a/docs/images/performances-flipper.png b/docs/images/performances-flipper.png new file mode 100644 index 000000000..f59556710 Binary files /dev/null and b/docs/images/performances-flipper.png differ diff --git a/ios/Podfile b/ios/Podfile index 4de7794a0..8fff81e15 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -60,6 +60,8 @@ target 'CozyReactNative' do pod 'RNDeviceInfo', :path => '../node_modules/react-native-device-info' + pod 'OpenSSL-Universal', :modular_headers => true, :configurations => ['Release'] + target 'CozyReactNativeDev' do inherit! :complete # Pods for testing diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6b2e2ed6a..6b2ffa396 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -179,12 +179,19 @@ PODS: - MLImage (= 1.0.0-beta2) - MLKitCommon (~> 5.0) - Protobuf (~> 3.12) + - MMKV (2.0.0): + - MMKVCore (~> 2.0.0) + - MMKVCore (2.0.0) - nanopb (2.30908.0): - nanopb/decode (= 2.30908.0) - nanopb/encode (= 2.30908.0) - nanopb/decode (2.30908.0) - nanopb/encode (2.30908.0) - NVHTarGzip (1.0.1) + - op-sqlite (6.2.11): + - React + - React-callinvoker + - React-Core - OpenSSL-Universal (1.1.1100) - PromisesObjC (2.4.0) - Protobuf (3.28.2) @@ -1079,18 +1086,43 @@ PODS: - React-Core - react-native-flipper (0.269.0): - React-Core + - react-native-get-random-values (1.11.0): + - React-Core - react-native-gzip (2.0.0): - NVHTarGzip - React - react-native-idle-timer (2.2.1): - React-Core + - react-native-mail (6.1.1): + - React-Core - react-native-mlkit-ocr (0.3.0): - GoogleMLKit/TextRecognition (= 2.6.0) - React + - react-native-mmkv (2.12.2): + - glog + - MMKV (>= 1.3.3) + - RCT-Folly (= 2022.05.16.00) + - React-Core - react-native-netinfo (9.5.0): - React-Core + - react-native-performance (5.1.2): + - React-Core - react-native-print (0.11.0): - React-Core + - react-native-quick-base64 (2.1.2): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core + - react-native-quick-crypto (0.7.6): + - glog + - OpenSSL-Universal + - RCT-Folly (= 2022.05.16.00) + - React + - React-Core + - react-native-quick-sqlite (8.0.6): + - React + - React-callinvoker + - React-Core - react-native-receive-sharing-intent (2.2.1): - React-Core - react-native-restart (0.0.27): @@ -1373,6 +1405,8 @@ DEPENDENCIES: - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - libevent (~> 2.1.12) + - "op-sqlite (from `../node_modules/@op-engineering/op-sqlite`)" + - OpenSSL-Universal - OpenSSL-Universal (= 1.1.1100) - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) @@ -1407,11 +1441,18 @@ DEPENDENCIES: - "react-native-cookies (from `../node_modules/@react-native-cookies/cookies`)" - react-native-document-scanner-plugin (from `../node_modules/react-native-document-scanner-plugin`) - react-native-flipper (from `../node_modules/react-native-flipper`) + - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) - "react-native-gzip (from `../node_modules/@fengweichong/react-native-gzip`)" - react-native-idle-timer (from `../node_modules/react-native-idle-timer`) + - react-native-mail (from `../node_modules/react-native-mail`) - react-native-mlkit-ocr (from `../node_modules/react-native-mlkit-ocr`) + - react-native-mmkv (from `../node_modules/react-native-mmkv`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" + - react-native-performance (from `../node_modules/react-native-performance`) - react-native-print (from `../node_modules/react-native-print`) + - react-native-quick-base64 (from `../node_modules/react-native-quick-base64`) + - react-native-quick-crypto (from `../node_modules/react-native-quick-crypto`) + - react-native-quick-sqlite (from `../node_modules/react-native-quick-sqlite`) - "react-native-receive-sharing-intent (from `../node_modules/@mythologi/react-native-receive-sharing-intent`)" - react-native-restart (from `../node_modules/react-native-restart`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) @@ -1494,6 +1535,8 @@ SPEC REPOS: - MLKitTextRecognition - MLKitTextRecognitionCommon - MLKitVision + - MMKV + - MMKVCore - nanopb - NVHTarGzip - OpenSSL-Universal @@ -1516,6 +1559,8 @@ EXTERNAL SOURCES: hermes-engine: :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" :tag: hermes-2024-04-29-RNv0.73.8-644c8be78af1eae7c138fa4093fb87f0f4f8db85 + op-sqlite: + :path: "../node_modules/@op-engineering/op-sqlite" RCT-Folly: :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTRequired: @@ -1576,16 +1621,30 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-document-scanner-plugin" react-native-flipper: :path: "../node_modules/react-native-flipper" + react-native-get-random-values: + :path: "../node_modules/react-native-get-random-values" react-native-gzip: :path: "../node_modules/@fengweichong/react-native-gzip" react-native-idle-timer: :path: "../node_modules/react-native-idle-timer" + react-native-mail: + :path: "../node_modules/react-native-mail" react-native-mlkit-ocr: :path: "../node_modules/react-native-mlkit-ocr" + react-native-mmkv: + :path: "../node_modules/react-native-mmkv" react-native-netinfo: :path: "../node_modules/@react-native-community/netinfo" + react-native-performance: + :path: "../node_modules/react-native-performance" react-native-print: :path: "../node_modules/react-native-print" + react-native-quick-base64: + :path: "../node_modules/react-native-quick-base64" + react-native-quick-crypto: + :path: "../node_modules/react-native-quick-crypto" + react-native-quick-sqlite: + :path: "../node_modules/react-native-quick-sqlite" react-native-receive-sharing-intent: :path: "../node_modules/@mythologi/react-native-receive-sharing-intent" react-native-restart: @@ -1723,8 +1782,11 @@ SPEC CHECKSUMS: MLKitTextRecognition: 8b0e0023a4babc66ca83d8b82864e57931164445 MLKitTextRecognitionCommon: 3e84602c928fe2b775fae81376f2136324cbd763 MLKitVision: e87dc3f2e456a6ab32361ebd985e078dd2746143 + MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801 + MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 NVHTarGzip: 74cc227b902e5725900d37eb6d79b57e93005a73 + op-sqlite: b4f61f3085ea3b70f264f3a64b724c81ab117e58 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 Protobuf: 28c89b24435762f60244e691544ed80f50d82701 @@ -1758,11 +1820,18 @@ SPEC CHECKSUMS: react-native-cookies: f54fcded06bb0cda05c11d86788020b43528a26c react-native-document-scanner-plugin: df5b82df67ff612262c40c26ef2c8239c5af5c55 react-native-flipper: 2a568e19c9d7051f01bf9725f886474fc818ba05 + react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 react-native-gzip: 5ffb84bf191c7cd135338eca748317bc466d41a1 react-native-idle-timer: f1920a59fe776340d004ff9de13c4a6eedcc8807 + react-native-mail: 8fdcd3aef007c33a6877a18eb4cf7447a1d4ce4a react-native-mlkit-ocr: 72cdbde86f8d29cba26cf9fa0a1865fe45c8f8d6 + react-native-mmkv: 1fdc81aa70c1aba09370718e6a63a09cbbbac8d2 react-native-netinfo: 48c5f79a84fbc3ba1d28a8b0d04adeda72885fa8 + react-native-performance: ff93f8af3b2ee9519fd7879896aa9b8b8272691d react-native-print: f704aef52d931bfce6d1d84351dbb5232d7ecb89 + react-native-quick-base64: e1ea036b3dec44c6da2439bd62881a09de614b23 + react-native-quick-crypto: 070488f041b282af3e375e511490061f1cf50474 + react-native-quick-sqlite: e0e23b749382a85e4b57146f753de737a6c3a9e1 react-native-receive-sharing-intent: 0c21b8e80f629a73341f2566ce9b99df8124bb10 react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162 react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc @@ -1816,6 +1885,6 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: 66a97477b94264cc4e49990c8fe6b153260d871d -PODFILE CHECKSUM: e53a16a804de17b86c29f5452d36e0ae6205ef23 +PODFILE CHECKSUM: 5ca14179a4e0b17ebbb7dd74c2a5fcb845f82a9d -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/package.json b/package.json index 3e1180711..61cda1f1b 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "ios": "react-native run-ios", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", "log:android": "adb logcat *:S ReactNative:V ReactNativeJS:V", + "perf:convert": "yarn tsc --project scripts && node scripts/dist/convert-perf-logs.cmd.js", "postinstall": "patch-package && yarn install:scripts && react-native setup-ios-permissions && pod-install", "pre-commit": "yarn lint", "start": "react-native start", @@ -35,9 +36,12 @@ "storybook:update": "sb-rn-get-stories" }, "dependencies": { + "@craftzdog/pouchdb-collate-react-native": "^7.3.0", + "@craftzdog/react-native-buffer": "^6.0.5", "@fengweichong/react-native-gzip": "github:cozy/react-native-gzip#1.1.0", "@mythologi/react-native-receive-sharing-intent": "2.2.1", "@notifee/react-native": "^7.8.0", + "@op-engineering/op-sqlite": "^6.2.11", "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "^7.2.0", "@react-native-clipboard/clipboard": "^1.13.2", @@ -55,14 +59,18 @@ "@sentry/integrations": "7.114.0", "@sentry/react-native": "5.33.1", "base-64": "^1.0.0", - "cozy-client": "^48.5.0", + "cozy-client": "^54.0.0", "cozy-clisk": "^0.38.1", + "cozy-dataproxy-lib": "^3.4.0", "cozy-device-helper": "^2.7.0", "cozy-flags": "^3.2.0", - "cozy-intent": "^2.22.0", + "cozy-intent": "^2.23.0", "cozy-logger": "^1.10.0", "cozy-minilog": "3.3.1", + "cozy-pouch-link": "^54.0.0", + "cozy-realtime": "^5.6.4", "date-fns": "2.29.3", + "events": "^3.3.0", "html-entities": "^2.3.3", "i18next": "23.7.16", "i18next-intervalplural-postprocessor": "3.0.0", @@ -74,6 +82,11 @@ "patch-package": "^8.0.0", "post-me": "^0.4.5", "postinstall-postinstall": "^2.1.0", + "pouchdb-adapter-http": "^8.0.1", + "pouchdb-adapter-react-native-sqlite": "^4.0.5", + "pouchdb-core": "^8.0.1", + "pouchdb-mapreduce": "^8.0.1", + "pouchdb-replication": "^8.0.1", "react": "18.2.0", "react-dom": "18.2.0", "react-i18next": "14.0.0", @@ -92,6 +105,7 @@ "react-native-flipper": "^0.269.0", "react-native-fs": "^2.20.0", "react-native-gesture-handler": "1.10.3", + "react-native-get-random-values": "^1.11.0", "react-native-google-play-integrity": "github:cozy/react-native-google-play-integrity#1.0.1", "react-native-iap": "^12.11.0", "react-native-idle-timer": "^2.2.1", @@ -100,11 +114,20 @@ "react-native-ios11-devicecheck": "https://github.com/cozy/react-native-devicecheck#app-attest-v0.1", "react-native-keychain": "^8.0.0", "react-native-localize": "2.2.6", + "react-native-mail": "^6.1.1", "react-native-mask-input": "1.2.1", "react-native-mlkit-ocr": "^0.3.0", + "react-native-mmkv": "2.12.2", + "react-native-mmkv-flipper-plugin": "^1.0.0", + "react-native-performance": "^5.1.2", + "react-native-performance-flipper-reporter": "^5.0.0", "react-native-permissions": "^3.9.3", "react-native-play-install-referrer": "^1.1.8", "react-native-print": "0.11.0", + "react-native-quick-base64": "2.1.2", + "react-native-quick-crypto": "^0.7.6", + "react-native-quick-sqlite": "8.0.6", + "react-native-quick-websql": "^0.3.0", "react-native-restart": "^0.0.27", "react-native-safe-area-context": "^4.5.0", "react-native-screens": "3.32.0", diff --git a/patches/react-native-performance-flipper-reporter+5.0.0.patch b/patches/react-native-performance-flipper-reporter+5.0.0.patch new file mode 100644 index 000000000..8ba70e9e7 --- /dev/null +++ b/patches/react-native-performance-flipper-reporter+5.0.0.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/react-native-performance-flipper-reporter/src/index.js b/node_modules/react-native-performance-flipper-reporter/src/index.js +index e99d526..bf1f6f1 100644 +--- a/node_modules/react-native-performance-flipper-reporter/src/index.js ++++ b/node_modules/react-native-performance-flipper-reporter/src/index.js +@@ -163,7 +163,7 @@ export function setupDefaultFlipperReporter() { + name: entry.name, + startTime: entry.startTime, + duration: entry.duration, +- category: 'App', ++ category: entry.detail?.category || 'App', + })) + ); + }, diff --git a/patches/redux+4.2.1.patch b/patches/redux+4.2.1.patch new file mode 100644 index 000000000..268876a6a --- /dev/null +++ b/patches/redux+4.2.1.patch @@ -0,0 +1,61 @@ +diff --git a/node_modules/redux/lib/redux.js b/node_modules/redux/lib/redux.js +index 8a39141..752ab8e 100644 +--- a/node_modules/redux/lib/redux.js ++++ b/node_modules/redux/lib/redux.js +@@ -3,6 +3,11 @@ + Object.defineProperty(exports, '__esModule', { value: true }); + + var _objectSpread = require('@babel/runtime/helpers/objectSpread2'); ++// Cozy override: Add react-native-performance measurements ++//* ++var uniqueId = require('lodash/uniqueId'); ++var rnperformance = require('react-native-performance').default; ++//*/ + + function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } + +@@ -279,6 +284,11 @@ function createStore(reducer, preloadedState, enhancer) { + + + function dispatch(action) { ++ // Cozy override: Add react-native-performance measurements ++ //* ++ const markName = `dispatch ${uniqueId()}` ++ rnperformance.mark(markName) ++ //*/ + if (!isPlainObject(action)) { + throw new Error(process.env.NODE_ENV === "production" ? formatProdErrorMessage(7) : "Actions must be plain objects. Instead, the actual type was: '" + kindOf(action) + "'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples."); + } +@@ -302,9 +312,32 @@ function createStore(reducer, preloadedState, enhancer) { + + for (var i = 0; i < listeners.length; i++) { + var listener = listeners[i]; ++ // Cozy override: Add react-native-performance measurements ++ //* ++ const markNamecurrentReducer = `dispatchlistener ${uniqueId()}` ++ rnperformance.mark(markNamecurrentReducer) ++ //*/ + listener(); ++ // Cozy override: Add react-native-performance measurements ++ //* ++ rnperformance.measure(markNamecurrentReducer, { ++ start: markNamecurrentReducer, ++ detail: { ++ category: 'REDUX' ++ } ++ }) ++ //*/ + } + ++ // Cozy override: Add react-native-performance measurements ++ //* ++ rnperformance.measure(markName, { ++ start: markName, ++ detail: { ++ category: 'REDUX' ++ } ++ }) ++ //*/ + return action; + } + /** diff --git a/scripts/convert-perf-logs.cmd.ts b/scripts/convert-perf-logs.cmd.ts new file mode 100644 index 000000000..5cf9721b2 --- /dev/null +++ b/scripts/convert-perf-logs.cmd.ts @@ -0,0 +1,227 @@ +// This script can be called using `yarn perf:convert ` + +import path from 'path' + +import fs from 'fs-extra' + +interface PerfEntry { + name: string + startTime: number + duration: number + initiatorType: string + entryType: string + detail?: { + category?: string + } +} + +interface TrackInfo { + color: string + track: string +} + +const convertFile = (): void => { + const sourcePath = process.argv[2] + + if (!sourcePath) { + console.error( + '⚠️ Missing file path argument. Please provide a path to the source performance logs file' + ) + return + } + + const sourceFile = fs.readFileSync(sourcePath, 'utf-8') + + const performanceLogs = JSON.parse(sourceFile) as PerfEntry[] + + const minTimestamp = + Math.min(...performanceLogs.map(entry => entry.startTime)) * 1000 + const maxTimestamp = + Math.max( + ...performanceLogs.map(entry => entry.startTime + entry.duration) + ) * 1000 + + const final = { + metadata: { + source: 'Cozy Flagship App', + startTime: '2024-11-24T11:41:40.862Z', + dataOrigin: 'TraceEvents' + }, + traceEvents: [ + { + args: { + data: { + frameTreeNodeId: 1602, + frames: [ + { + frame: '58D933E9AC2121C7555CECAF5DABE9EA', + isInPrimaryMainFrame: true, + isOutermostMainFrame: true, + name: '', + processId: 1119, + url: 'chrome://newtab/' + } + ], + persistentIds: true + } + }, + cat: 'disabled-by-default-devtools.timeline', + name: 'TracingStartedInBrowser', + ph: 'I', + pid: 1083, + s: 't', + tid: 259, + ts: minTimestamp, + tts: 1604839675 + }, + { + args: {}, + cat: 'disabled-by-default-devtools.timeline', + name: 'RunTask', + ph: 'B', + pid: 1094, + tid: 20483, + ts: maxTimestamp + }, + ...performanceLogs.map((entry, index) => { + return { + args: { + startTime: entry.startTime, + detail: JSON.stringify({ + devtools: { + dataType: 'track-entry', + ...entryToTrack(entry), + trackGroup: 'Cozy Flagship App' + } + }) + }, + cat: 'blink.user_timing', + id2: { + local: '0x' + index.toString(16) + }, + name: entry.name, + ph: 'b', + pid: index, + tid: 259, + ts: entry.startTime * 1000 + } + }), + ...performanceLogs.map((entry, index) => { + return { + args: {}, + cat: 'blink.user_timing', + id2: { + local: '0x' + index.toString(16) + }, + name: entry.name, + ph: 'e', + pid: index, + tid: 259, + ts: (entry.startTime + entry.duration) * 1000 + } + }), + ...marksToEntry('nativeLaunch', performanceLogs), + ...marksToEntry('reactContextThread', performanceLogs), + ...marksToEntry('processCoreReactPackage', performanceLogs), + ...marksToEntry('buildNativeModuleRegistry', performanceLogs), + ...marksToEntry('loadReactNativeSoFile', performanceLogs), + ...marksToEntry('createCatalystInstance', performanceLogs), + ...marksToEntry('createReactContext', performanceLogs), + ...marksToEntry('preSetupReactContext', performanceLogs), + ...marksToEntry('setupReactContext', performanceLogs), + ...marksToEntry('attachMeasuredRootViews', performanceLogs), + ...marksToEntry('createUiManagerModule', performanceLogs), + ...marksToEntry('createViewManagers', performanceLogs), + ...marksToEntry('createUiManagerModuleConstants', performanceLogs), + ...marksToEntry('runJsBundle', performanceLogs) + ] + } + + const sourceFileName = path.basename(sourcePath, '.json') + const sourceBasePath = path.dirname(sourcePath) + const destinationPath = path.join( + sourceBasePath, + `${sourceFileName}_converted.json` + ) + + fs.writeFileSync(destinationPath, JSON.stringify(final), 'utf-8') +} + +const entryToTrack = (entry: PerfEntry): TrackInfo => { + const category = entry.detail?.category + if (category) { + return { + color: 'primary', + track: category + } + } + if (entry.initiatorType === 'xmlhttprequest') { + return { + color: 'secondary', + track: 'network' + } + } + if (entry.entryType === 'mark') { + if (entry.name.includes('SplashScreen')) { + return { + color: 'primary-light', + track: 'splashscreen' + } + } + + return { + color: 'primary-light', + track: 'mark' + } + } + return { + color: 'tertiary', + track: 'global' + } +} + +const marksToEntry = (eventName: string, performanceLogs: PerfEntry[]) => { + console.log('Do', eventName) + const startMark = `${eventName}Start` + const endMark = `${eventName}End` + const startEvent = performanceLogs.find(e => e.name === startMark)! + const endEvent = performanceLogs.find(e => e.name === endMark)! + + return [ + { + args: { + startTime: startEvent.startTime, + detail: JSON.stringify({ + devtools: { + dataType: 'track-entry', + ...entryToTrack(startEvent), + trackGroup: 'Cozy Flagship App' + } + }) + }, + cat: 'blink.user_timing', + id2: { + local: '0x' + (performanceLogs.length + 1).toString(16) + }, + name: eventName, + ph: 'b', + pid: (performanceLogs.length + 1), + tid: 259, + ts: startEvent.startTime * 1000 + }, + { + args: {}, + cat: 'blink.user_timing', + id2: { + local: '0x' + (performanceLogs.length + 1).toString(16) + }, + name: eventName, + ph: 'e', + pid: (performanceLogs.length + 1), + tid: 259, + ts: (startEvent.startTime + (endEvent.startTime - startEvent.startTime)) * 1000 + } + ] +} + +convertFile() diff --git a/src/@types/cozy-client.d.ts b/src/@types/cozy-client.d.ts index 481e60b27..757fba26e 100644 --- a/src/@types/cozy-client.d.ts +++ b/src/@types/cozy-client.d.ts @@ -1,7 +1,14 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import 'cozy-client' -import { FileDocument, CozyClientDocument } from 'cozy-client/types/types' +import { QueryDefinition, CozyLink } from 'cozy-client' +import type { PerformanceAPI } from 'cozy-client/types/performances/types' +import { + FileDocument, + CozyClientDocument, + QueryOptions, + QueryResult +} from 'cozy-client/types/types' declare module 'cozy-client' { interface ClientOptions { @@ -21,6 +28,7 @@ declare module 'cozy-client' { token?: Token uri?: string warningForCustomHandlers?: boolean + performanceApi?: PerformanceAPI } interface OAuthOptions { @@ -105,6 +113,7 @@ declare module 'cozy-client' { export interface FileCollectionGetResult { data: { _id: string + _rev: string name: string path: string metadata?: { @@ -171,6 +180,13 @@ declare module 'cozy-client' { on: (event: string, callback: () => void) => void removeListener: (event: string, callback: () => void) => void logout: () => Promise + query: ( + queryDefinition: QueryDefinition, + options?: QueryOptions + ) => Promise + links: CozyLink[] + plugins: Record + registerPlugin: (plugin: unknown, options?: unknown) => void } export const createMockClient = (options?: ClientOptions): CozyClient => diff --git a/src/App.js b/src/App.js index beb1f458b..238325336 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,13 @@ import { NavigationContainer } from '@react-navigation/native' import { decode, encode } from 'base-64' import React, { useEffect, useState } from 'react' -import { StatusBar, StyleSheet, View } from 'react-native' +import { + StatusBar, + StyleSheet, + View, + ActivityIndicator, + InteractionManager +} from 'react-native' import { SafeAreaProvider } from 'react-native-safe-area-context' import { Provider } from 'react-redux' import { PersistGate } from 'redux-persist/integration/react' @@ -10,6 +16,10 @@ import FlipperAsyncStorage from 'rn-flipper-async-storage-advanced' import { CozyProvider, useClient } from 'cozy-client' import { NativeIntentProvider } from 'cozy-intent' +import rnperformance, { + configurePerformances +} from '/app/domain/performances/measure' +import { usePerformancesUniversalLinks } from '/app/domain/performances/hooks/usePerformancesUniversalLinks' import { RootNavigator } from '/AppRouter' import * as RootNavigation from '/libs/RootNavigation' import NetStatusBoundary from '/libs/services/NetStatusBoundary' @@ -31,7 +41,7 @@ import { useCozyEnvironmentOverride } from '/hooks/useCozyEnvironmentOverride' import { useNotifications } from '/hooks/useNotifications' import { useSynchronizeOnInit } from '/hooks/useSynchronizeOnInit' import { useInitBackup } from '/app/domain/backup/hooks' -import { useNetService } from '/libs/services/NetService' +import { configureNetService, useNetService } from '/libs/services/NetService' import { withSentry } from '/libs/monitoring/Sentry' import { ThemeProvider } from '/app/theme/ThemeProvider' import { useInitI18n } from '/locales/useInitI18n' @@ -41,6 +51,7 @@ import { useGeolocationTracking } from '/app/domain/geolocation/hooks/useGeoloca import { OsReceiveProvider } from '/app/view/OsReceive/OsReceiveProvider' import { ErrorProvider } from '/app/view/Error/ErrorProvider' import { LoadingOverlayProvider } from '/app/view/Loading/LoadingOverlayProvider' +import { useOfflineDebugUniversalLinks } from '/app/domain/offline/hooks/useOfflineDebugUniversalLinks' import { OsReceiveApi } from '/app/domain/osReceive/services/OsReceiveApi' import { useOsReceiveDispatch, @@ -59,14 +70,33 @@ import { LauncherContextProvider } from '/screens/home/hooks/useLauncherContext' import LauncherView from '/screens/konnectors/LauncherView' +import { makeImportantFilesAvailableOfflineInBackground } from '/app/domain/io.cozy.files/importantFiles' +import { useOfflineReplicationOnRealtime } from '/app/domain/offline/hooks/useOfflineReplicationOnRealtime' import { useShareFiles } from '/app/domain/osReceive/services/shareFilesService' import { ClouderyOffer } from '/app/view/IAP/ClouderyOffer' import { useDimensions } from '/libs/dimensions' import { configureFileLogger } from '/app/domain/logger/fileLogger' import { useColorScheme } from '/app/theme/colorScheme' +import { + hasMigratedFromAsyncStorage, + migrateFromAsyncStorage, + storage +} from '/libs/localStore/storage' +if (__DEV__) { + require('react-native-performance-flipper-reporter').setupDefaultFlipperReporter() + require('react-native-mmkv-flipper-plugin').initializeMMKVFlipper({ + default: storage + }) +} + +configurePerformances() +configureNetService() configureFileLogger() +const markStartName = rnperformance.mark('AppStart') +rnperformance.measure({ markName: markStartName }) + // Polyfill needed for cozy-client connection if (!global.btoa) { global.btoa = encode @@ -84,6 +114,7 @@ const LoggedInWrapper = ({ children }) => { // eslint-disable-next-line react/display-name const App = ({ setClient }) => { + const [markNameApp] = useState(() => rnperformance.mark('App')) const client = useClient() useSynchronizeOnInit() @@ -100,6 +131,9 @@ const App = ({ setClient }) => { useNotifications() useGeolocationTracking() useCozyEnvironmentOverride() + useOfflineReplicationOnRealtime() + useOfflineDebugUniversalLinks(client) + usePerformancesUniversalLinks(client) const { LauncherDialog, @@ -112,6 +146,29 @@ const App = ({ setClient }) => { setLauncherContext } = useLauncherContext() + useEffect(() => { + if (!client) { + return + } + makeImportantFilesAvailableOfflineInBackground(client) + }, [client]) + + useEffect(() => { + if (!isLoading) { + rnperformance.measure({ + markName: markNameApp, + measureName: 'useAppBootstrap' + }) + } + }, [isLoading, markNameApp]) + + useEffect(() => { + rnperformance.measure({ + markName: markNameApp, + measureName: 'Mount ' + }) + }, [markNameApp]) + if (isLoading) { return null } @@ -135,12 +192,20 @@ const App = ({ setClient }) => { } const InnerNav = ({ client, setClient }) => { + const [markNameInnerNav] = useState(() => rnperformance.mark('InnerNav')) useDimensions() const colors = getColors() const osReceiveState = useOsReceiveState() const osReceiveDispatch = useOsReceiveDispatch() const { shareFiles } = useShareFiles() + useEffect(() => { + rnperformance.measure({ + markName: markNameInnerNav, + measureName: 'Mount ' + }) + }, [markNameInnerNav]) + return ( { ) } -const Nav = ({ client, setClient }) => ( - - - - - - - - - - {client && } - - - -) +const Nav = ({ client, setClient }) => { + const [markNameNav] = useState(() => rnperformance.mark('Nav')) + useEffect(() => { + rnperformance.measure({ + markName: markNameNav, + measureName: 'Mount