fix(mobile): upgrade react-native-quick-crypto for Android 16KB page size (#7480)

* fix(mobile): upgrade react-native-quick-crypto for Android 16KB page size support

Upgrade react-native-quick-crypto from 0.7.x to 1.0.17 to support
Android 16KB page size requirement. Add react-native-nitro-modules and
react-native-quick-base64 as new peer dependencies. Update
react-native-device-crypto patch to remove deprecated jcenter()
repository references for Gradle 9.0 compatibility.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* chore(mobile): remove unused config/ethers.ts

Dead code — crypto-shims.ts in platform/ is the active version
imported by the root layout.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* fix(mobile): deduplicate react-native-quick-base64 via resolution

Add root resolution to force a single copy of react-native-quick-base64
(2.2.2), avoiding version skew between the direct dependency and the
transitive one from @craftzdog/react-native-buffer.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* fix(mobile): upgrade react-native-mmkv to v4 and pin MMKV pod

- Upgrade react-native-mmkv from 3.x to 4.2.0 (Nitro-based rewrite)
- Migrate API: new MMKV() -> createMMKV(), .delete() -> .remove()
- Pin MMKV pod to ~> 2.3.0 in notification extension plugin instead
  of master branch, fixing sharedApplication build error in App
  Extensions
- Update expo plugin config.ts (source of truth for Podfile template)

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* docs: add solution doc for Expo native file editing workflow

Document that generated native files (ios/Podfile etc.) must be
modified through Expo config plugins, not directly. Also unignore
docs/solutions/ from .gitignore.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* fix(mobile): add jest mocks for react-native-nitro-modules and mmkv v4

MMKV v4 uses NitroModules under the hood, which requires mocking in
the test environment. Add mocks for both react-native-nitro-modules
and the new react-native-mmkv createMMKV API.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

---------

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
Daniel Dimitrov
2026-03-19 13:28:26 +01:00
committed by GitHub
co-authored by Hanzo Dev
parent 2c02ee2592
commit c166a5a44e
12 changed files with 214 additions and 86 deletions
+2 -1
View File
@@ -112,7 +112,8 @@ apps/tx-builder/vite.config.d.ts
# claude-workflows
todos/*
/docs/
/docs/plans/
/docs/todos/
.codemod
.serena
@@ -1,3 +1,23 @@
diff --git a/android/build.gradle b/android/build.gradle
index 8eb758ed063d1905f538f72e3ee55a8c41e01d18..d1ee770a3594017357465e031c2edb4aae6a448c 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -3,7 +3,6 @@ buildscript {
repositories {
google()
mavenCentral()
- jcenter()
}
dependencies {
@@ -52,7 +51,6 @@ repositories {
}
google()
mavenCentral()
- jcenter()
}
dependencies {
diff --git a/android/src/main/java/com/reactnativedevicecrypto/Helpers.java b/android/src/main/java/com/reactnativedevicecrypto/Helpers.java
index 03fd79e8a389f33a2841ecf0e177ffc9ea01b78c..d903b5dc4b0110dab9a8c1a648d349200dd4e20f 100644
--- a/android/src/main/java/com/reactnativedevicecrypto/Helpers.java
+4 -2
View File
@@ -122,12 +122,14 @@
"react-native-gesture-handler": "~2.30.0",
"react-native-keyboard-controller": "1.20.7",
"react-native-keychain": "^10.0.0",
"react-native-mmkv": "^3.3.3",
"react-native-mmkv": "^4.2.0",
"react-native-nitro-modules": "^0.35.2",
"react-native-pager-view": "8.0.0",
"react-native-permissions": "^5.4.2",
"react-native-progress": "^5.0.1",
"react-native-qrcode-styled": "^0.4.0",
"react-native-quick-crypto": "^0.7.11",
"react-native-quick-base64": "^2.2.2",
"react-native-quick-crypto": "^1.0.17",
"react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.23.0",
-42
View File
@@ -1,42 +0,0 @@
//TODO: the interface of ethersjs register is not compatible
// with the interface of suggested crypto functions
// that is why we do casting.
// reference: https://docs.ethers.org/v6/cookbook/react-native/
import { BytesLike, ethers } from 'ethers'
import crypto from 'react-native-quick-crypto'
ethers.randomBytes.register((length) => {
return new Uint8Array(crypto.randomBytes(length))
})
ethers.computeHmac.register((algo, key, data) => {
return crypto.createHmac(algo, key).update(data).digest()
})
ethers.pbkdf2.register(
(
password: Uint8Array,
salt: Uint8Array,
iterations: number,
keylen: number,
algo: 'sha256' | 'sha512',
): BytesLike => {
return crypto.pbkdf2Sync(password, salt, iterations, keylen, algo) as unknown as BytesLike
},
)
ethers.sha256.register((data) => {
return crypto
.createHash('sha256')
.update(data as unknown as string)
.digest()
})
ethers.sha512.register((data) => {
return crypto
.createHash('sha512')
.update(data as unknown as string)
.digest()
})
@@ -1,4 +1,4 @@
import { MMKV } from 'react-native-mmkv'
import { createMMKV } from 'react-native-mmkv'
/**
* Shared MMKV instance for extension storage
@@ -6,4 +6,4 @@ import { MMKV } from 'react-native-mmkv'
* Fun fact: it's named extensionStorage, because it was supposed to be used only in
* ios's service extension. Now it is also used on Android, but in headless mode.
*/
export const extensionStorage = new MMKV({ id: 'extension' })
export const extensionStorage = createMMKV({ id: 'extension' })
@@ -1,10 +1,10 @@
import { MMKV } from 'react-native-mmkv'
import { createMMKV, type MMKV } from 'react-native-mmkv'
let processedMessagesStorage: MMKV | null = null
const getProcessedMessagesStorage = (): MMKV => {
if (!processedMessagesStorage) {
processedMessagesStorage = new MMKV({ id: 'processed-messages' })
processedMessagesStorage = createMMKV({ id: 'processed-messages' })
}
return processedMessagesStorage
}
@@ -33,7 +33,7 @@ export const checkAndMarkMessageProcessed = (messageId: string, maxStoredMessage
const allKeys = storage.getAllKeys().filter((key) => key.startsWith('processed_'))
if (allKeys.length > maxStoredMessages) {
const oldKeys = allKeys.slice(0, allKeys.length - maxStoredMessages)
oldKeys.forEach((key) => storage.delete(key))
oldKeys.forEach((key) => storage.remove(key))
console.log('[MessageDeduplication] Cleaned up', oldKeys.length, 'old processed messages')
}
@@ -46,6 +46,6 @@ export const checkAndMarkMessageProcessed = (messageId: string, maxStoredMessage
export const clearProcessedMessages = (): void => {
const storage = getProcessedMessagesStorage()
const allKeys = storage.getAllKeys().filter((key) => key.startsWith('processed_'))
allKeys.forEach((key) => storage.delete(key))
allKeys.forEach((key) => storage.remove(key))
console.log('[MessageDeduplication] Cleared all processed messages:', allKeys.length, 'records')
}
+5 -5
View File
@@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
import { Storage } from 'redux-persist'
import { MMKV } from 'react-native-mmkv'
import { createMMKV } from 'react-native-mmkv'
import { mapStorageTypeToIds, STORAGE_IDS, STORAGE_TYPES } from './constants'
const safeStorage = new MMKV({
const safeStorage = createMMKV({
id: STORAGE_IDS.SAFE,
})
@@ -17,7 +17,7 @@ export const reduxStorage: Storage = {
return Promise.resolve(value)
},
removeItem: (key) => {
safeStorage.delete(key)
safeStorage.remove(key)
return Promise.resolve()
},
}
@@ -59,11 +59,11 @@ export class safeMMKVStorage {
static clearAllStorages() {
Object.keys(STORAGE_IDS).forEach((id) => {
const storage = new MMKV({ id })
const storage = createMMKV({ id })
storage.clearAll()
})
const defaultStorage = new MMKV()
const defaultStorage = createMMKV()
defaultStorage.clearAll()
}
}
+22
View File
@@ -163,6 +163,28 @@ jest.mock('@react-native-clipboard/clipboard', () => ({
getString: jest.fn(),
}))
jest.mock('react-native-nitro-modules', () => ({
NitroModules: {
createHybridObject: jest.fn(),
},
}))
jest.mock('react-native-mmkv', () => {
const store = new Map<string, string | number | boolean>()
const mmkvMock = {
set: jest.fn((key: string, value: string | number | boolean) => store.set(key, value)),
getString: jest.fn((key: string) => store.get(key) as string | undefined),
getNumber: jest.fn((key: string) => store.get(key) as number | undefined),
getBoolean: jest.fn((key: string) => store.get(key) as boolean | undefined),
remove: jest.fn((key: string) => store.delete(key)),
clearAll: jest.fn(() => store.clear()),
getAllKeys: jest.fn(() => [...store.keys()]),
}
return {
createMMKV: jest.fn(() => ({ ...mmkvMock })),
}
})
jest.mock('react-native-quick-crypto', () => ({
default: {
randomBytes: jest.fn((size) => Buffer.alloc(size)),
@@ -0,0 +1,87 @@
---
module: Mobile
date: 2026-03-19
problem_type: workflow_issue
component: development_workflow
symptoms:
- "Podfile modified directly but changes lost on next prebuild/native regeneration"
- "Expo plugin config.ts updated but dist/ not rebuilt, so changes not applied"
- "MMKV pod pinned to master in Podfile instead of through Expo config plugin"
root_cause: missing_workflow_step
resolution_type: workflow_improvement
severity: high
tags: [expo, podfile, native-generation, expo-plugins, build-step, ios, mobile]
---
# Direct Native File Edits in Expo Project
## Problem
During an upgrade of `react-native-quick-crypto` and `react-native-mmkv`, the iOS `Podfile` was modified directly to:
1. Remove a `pod 'MMKV'` line
2. Add a `post_install` block with `MMKV_IOS_EXTENSION` preprocessor macro
3. Change the MMKV pod version pin
These changes were made to the generated `apps/mobile/ios/Podfile` rather than through the Expo config plugin that generates it (`expo-plugins/notification-service-ios`).
Additionally, when the Expo plugin source (`plugin/config.ts`) was correctly updated, the build step (`yarn build` in the plugin directory) was not run, so the compiled output in `dist/` was stale and the changes were not applied during the next prebuild.
## Root Cause
Two workflow gaps:
1. **Expo continuously regenerates native files.** The `ios/` and `android/` directories are generated artifacts in an Expo managed/custom dev client workflow. Direct edits to `Podfile`, `AppDelegate`, `Info.plist`, etc. will be overwritten on the next `expo prebuild`. All native configuration must flow through Expo config plugins or `app.config.ts`.
2. **Expo plugins with a build step require compilation.** The `notification-service-ios` plugin has a TypeScript source in `plugin/` and compiled output in `dist/`. The `app.plugin.js` entry point loads from `dist/`. Editing `plugin/config.ts` without running `yarn build` (or the plugin's build script) means the changes never reach the compiled output that Expo actually reads.
## Solution
### Rule 1: Never directly edit generated native files
- `apps/mobile/ios/Podfile` - generated, edit via Expo plugins
- `apps/mobile/ios/*.pbxproj` - generated, edit via Expo plugins
- `apps/mobile/android/build.gradle` - generated, edit via Expo plugins
Instead, modify the Expo config plugin that generates the content:
```
# The Podfile content for the notification extension is templated here:
expo-plugins/notification-service-ios/plugin/config.ts # PODFILE_MODIF_NEEDED constant
# NOT here:
apps/mobile/ios/Podfile # This is a generated output
```
### Rule 2: Always run the plugin build step after editing plugin source
```bash
# After editing any file in expo-plugins/notification-service-ios/plugin/
cd expo-plugins/notification-service-ios
yarn build # Compiles TS and copies ios-notification-service-files/ to dist/
# Then regenerate native project
cd apps/mobile
npx expo prebuild --clean # Regenerates ios/ and android/ from plugins
```
The build script from `package.json`:
```json
"build": "rm -rf dist && tsc && cp -a ./ios-notification-service-files dist/ios-notification-service-files/"
```
This copies both the compiled TypeScript AND the Swift/plist template files into `dist/`.
## Prevention
Before modifying any native iOS/Android file in an Expo project:
1. **Ask: "Is this file generated?"** If it's under `ios/` or `android/`, it almost certainly is.
2. **Find the config plugin** that generates or modifies it. Search `expo-plugins/` and `app.config.ts`.
3. **Edit the plugin source**, not the generated output.
4. **Run the plugin build step** if the plugin has one (`yarn build`).
5. **Run `expo prebuild`** to verify changes are applied correctly.
## Related
- `expo-plugins/notification-service-ios/` - The Expo plugin for iOS notification service extension
- `apps/mobile/ios/Podfile` - Generated Podfile (do not edit directly)
@@ -15,7 +15,7 @@ export const PODFILE_MODIF_NEEDED = `
$NotifeeExtension = true
target 'NotifeeNotificationServiceExtension' do
pod 'RNNotifeeCore', :path => '../../../node_modules/@notifee/react-native/RNNotifeeCore.podspec'
pod 'MMKV', :git => 'https://github.com/Tencent/MMKV.git', :branch => 'master'
pod 'MMKV', '~> 2.3.0'
pod 'CryptoSwift', '~> 1.8.3'
pod 'SwiftCryptoTokenFormatter', :git => 'https://github.com/compojoom/SwiftCryptoTokenFormatter', :branch => 'main'
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
+2 -1
View File
@@ -28,7 +28,8 @@
"stylus": "0.64.0",
"viem": "2.21.55",
"webpack": "5.97.1",
"@tamagui/static/react-native-web": "0.21.1"
"@tamagui/static/react-native-web": "0.21.1",
"react-native-quick-base64": "2.2.2"
},
"devDependencies": {
"@storybook/builder-webpack5": "^10.2.6",
+65 -28
View File
@@ -4079,13 +4079,13 @@ __metadata:
languageName: node
linkType: hard
"@craftzdog/react-native-buffer@npm:^6.0.5":
version: 6.0.5
resolution: "@craftzdog/react-native-buffer@npm:6.0.5"
"@craftzdog/react-native-buffer@npm:6.1.0":
version: 6.1.0
resolution: "@craftzdog/react-native-buffer@npm:6.1.0"
dependencies:
ieee754: "npm:^1.2.1"
react-native-quick-base64: "npm:^2.0.5"
checksum: 10/9963b430d0cd2af030605a7e81b3982dfec36603406d4f14d520e28936d24a79d69ca4e8a3b650734f6e45b12d87f1bd1ce86f0db11379103e7b1c024a2510d5
checksum: 10/78ab30297432f0f24445417ceefde6f2e8f0596f7fe6ff44d9bc0b509f4aaa5e141c23e1aca9db25678abd306802a07ed8bc3690f4b1a4375ac67a1391c1a224
languageName: node
linkType: hard
@@ -13424,12 +13424,14 @@ __metadata:
react-native-gesture-handler: "npm:~2.30.0"
react-native-keyboard-controller: "npm:1.20.7"
react-native-keychain: "npm:^10.0.0"
react-native-mmkv: "npm:^3.3.3"
react-native-mmkv: "npm:^4.2.0"
react-native-nitro-modules: "npm:^0.35.2"
react-native-pager-view: "npm:8.0.0"
react-native-permissions: "npm:^5.4.2"
react-native-progress: "npm:^5.0.1"
react-native-qrcode-styled: "npm:^0.4.0"
react-native-quick-crypto: "npm:^0.7.11"
react-native-quick-base64: "npm:^2.2.2"
react-native-quick-crypto: "npm:^1.0.17"
react-native-reanimated: "npm:4.2.1"
react-native-safe-area-context: "npm:~5.6.2"
react-native-screens: "npm:~4.23.0"
@@ -39039,11 +39041,11 @@ __metadata:
"react-native-device-crypto@patch:react-native-device-crypto@npm%3A0.1.7#~/.yarn/patches/react-native-device-crypto-npm-0.1.7-dbd2698fc4.patch":
version: 0.1.7
resolution: "react-native-device-crypto@patch:react-native-device-crypto@npm%3A0.1.7#~/.yarn/patches/react-native-device-crypto-npm-0.1.7-dbd2698fc4.patch::version=0.1.7&hash=4f3cef"
resolution: "react-native-device-crypto@patch:react-native-device-crypto@npm%3A0.1.7#~/.yarn/patches/react-native-device-crypto-npm-0.1.7-dbd2698fc4.patch::version=0.1.7&hash=2b3d9e"
peerDependencies:
react: "*"
react-native: "*"
checksum: 10/d689bbe212fe96f2675f0f17b61476727da5d6283441558b3e6313f7310d06fc02119597654b9bf8d5464e544193d286df95ce0926cf363e24687dfae88885ce
checksum: 10/0eed99b617e49ca1c5132aa7f97e2043c4d29987442b841288e6d77e9fcd04560026269e60ec41318b1046b31682ff6183e17d39ddad66a0bf491b2d4241a7eb
languageName: node
linkType: hard
@@ -39113,13 +39115,14 @@ __metadata:
languageName: node
linkType: hard
"react-native-mmkv@npm:^3.3.3":
version: 3.3.3
resolution: "react-native-mmkv@npm:3.3.3"
"react-native-mmkv@npm:^4.2.0":
version: 4.2.0
resolution: "react-native-mmkv@npm:4.2.0"
peerDependencies:
react: "*"
react-native: "*"
checksum: 10/61c3d61db0bf8c311c99ae9ac6cf93071a2cf8771fc0e92af828938b50be7631834d608126182d13aa97ed059e202ffc31dbfe2d84cc760a21e65893dc6a7866
react-native-nitro-modules: "*"
checksum: 10/8f0e919eb3add26d47a9d5e171672928c322c89ef92108375ffcc2829d40a5d1727f6b9bed771d1fc7abf76f80695e522d47699c8a891c36a98b04afd3a8d1d1
languageName: node
linkType: hard
@@ -39135,6 +39138,16 @@ __metadata:
languageName: node
linkType: hard
"react-native-nitro-modules@npm:^0.35.2":
version: 0.35.2
resolution: "react-native-nitro-modules@npm:0.35.2"
peerDependencies:
react: "*"
react-native: "*"
checksum: 10/22977e5e22c63fb91526c90fa59fcfe5c7f97978c583400f433a1d3ef8ee32fbceb0ad1b009b5bf0d1794e94a11716ad752fd7702c2d67fc808a0256c6e35488
languageName: node
linkType: hard
"react-native-pager-view@npm:8.0.0":
version: 8.0.0
resolution: "react-native-pager-view@npm:8.0.0"
@@ -39185,28 +39198,39 @@ __metadata:
languageName: node
linkType: hard
"react-native-quick-base64@npm:^2.0.5":
version: 2.1.2
resolution: "react-native-quick-base64@npm:2.1.2"
dependencies:
base64-js: "npm:^1.5.1"
"react-native-quick-base64@npm:2.2.2":
version: 2.2.2
resolution: "react-native-quick-base64@npm:2.2.2"
peerDependencies:
react: "*"
react-native: "*"
checksum: 10/20b205612417a6c39452ad3ebd205d74fcfaa244832ec9e717b87ee075bf28076bf2445e8f690aad8cf0be6f3da89b4148eb8379d0b71d1c953bd06f66cc7c56
checksum: 10/aeebf6f520830dc042201e2a72a381fb29d1376fe0acdf9e266c8a7e3499f45534b11b27b2241bf6cf007ff82874b6def9fa52d62f0027953fc2e6f8d0009266
languageName: node
linkType: hard
"react-native-quick-crypto@npm:^0.7.11":
version: 0.7.11
resolution: "react-native-quick-crypto@npm:0.7.11"
"react-native-quick-crypto@npm:^1.0.17":
version: 1.0.17
resolution: "react-native-quick-crypto@npm:1.0.17"
dependencies:
"@craftzdog/react-native-buffer": "npm:^6.0.5"
events: "npm:^3.3.0"
readable-stream: "npm:^4.5.2"
"@craftzdog/react-native-buffer": "npm:6.1.0"
events: "npm:3.3.0"
readable-stream: "npm:4.5.2"
safe-buffer: "npm:^5.2.1"
string_decoder: "npm:^1.3.0"
util: "npm:^0.12.5"
checksum: 10/a14ece6ef32d80f5e19a559f332512dc5e850fbe0f93e09de091d1dec9bd22ee2f20a23d3a52f8441f01f3c58c161c3e4cfa2febbc3a2b994804c8e21aa856b7
util: "npm:0.12.5"
peerDependencies:
expo: ">=48.0.0"
expo-build-properties: "*"
react: "*"
react-native: "*"
react-native-nitro-modules: ">=0.29.1"
react-native-quick-base64: ">=2.1.0"
peerDependenciesMeta:
expo:
optional: true
expo-build-properties:
optional: true
checksum: 10/8aeac9346e13a797160a33a2f2222ea571911df7ef1e68b38731ea3f07d499090c3d78ad9f63fc4101de019e92bac8a0d447b20d988b9990abdc7c583e4ad913
languageName: node
linkType: hard
@@ -39688,6 +39712,19 @@ __metadata:
languageName: node
linkType: hard
"readable-stream@npm:4.5.2":
version: 4.5.2
resolution: "readable-stream@npm:4.5.2"
dependencies:
abort-controller: "npm:^3.0.0"
buffer: "npm:^6.0.3"
events: "npm:^3.3.0"
process: "npm:^0.11.10"
string_decoder: "npm:^1.3.0"
checksum: 10/01b128a559c5fd76a898495f858cf0a8839f135e6a69e3409f986e88460134791657eb46a2ff16826f331682a3c4d0c5a75cef5e52ef259711021ba52b1c2e82
languageName: node
linkType: hard
"readable-stream@npm:^2.0.2, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.6":
version: 2.3.8
resolution: "readable-stream@npm:2.3.8"
@@ -39714,7 +39751,7 @@ __metadata:
languageName: node
linkType: hard
"readable-stream@npm:^4.0.0, readable-stream@npm:^4.5.2":
"readable-stream@npm:^4.0.0":
version: 4.6.0
resolution: "readable-stream@npm:4.6.0"
dependencies:
@@ -45100,7 +45137,7 @@ __metadata:
languageName: node
linkType: hard
"util@npm:^0.12.4, util@npm:^0.12.5":
"util@npm:0.12.5, util@npm:^0.12.4, util@npm:^0.12.5":
version: 0.12.5
resolution: "util@npm:0.12.5"
dependencies: