Skip to content

Commit ae38fbd

Browse files
feat: implement AsyncStorage to MMKV migration and update storage handling
1 parent 9ce4892 commit ae38fbd

7 files changed

Lines changed: 313 additions & 15 deletions

File tree

App.js

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import React, { useEffect } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import { Provider } from 'react-redux';
33
import { PersistGate } from 'redux-persist/integration/react';
4-
import { persistor, store } from './src/Redux/Store';
4+
import { persistor, store, performStorageMigration } from './src/Redux/Store';
55
import { GestureHandlerRootView } from 'react-native-gesture-handler';
66
import { RootNavigation } from './src/Navigation';
77
import Loading from './src/Components/UIComp/Loading';
@@ -19,6 +19,39 @@ import {
1919
} from './src/InkNest-Externals/Community/Logic/CommunityActions';
2020
import NotificationSubscriptionBootstrapper from './src/InkNest-Externals/Notifications/components/NotificationSubscriptionBootstrapper';
2121

22+
/**
23+
* Migration gate component that handles AsyncStorage -> MMKV migration
24+
* before rendering the app
25+
*/
26+
function MigrationGate({ children }) {
27+
const [isMigrating, setIsMigrating] = useState(true);
28+
const [migrationError, setMigrationError] = useState(null);
29+
30+
useEffect(() => {
31+
async function runMigration() {
32+
try {
33+
// Perform migration from AsyncStorage to MMKV
34+
await performStorageMigration();
35+
setIsMigrating(false);
36+
} catch (error) {
37+
console.error('Migration failed:', error);
38+
crashlytics().recordError(error);
39+
setMigrationError(error);
40+
// Even on error, allow app to continue (may lose old data but app works)
41+
setIsMigrating(false);
42+
}
43+
}
44+
45+
runMigration();
46+
}, []);
47+
48+
if (isMigrating) {
49+
return <Loading />;
50+
}
51+
52+
return children;
53+
}
54+
2255
/**
2356
* The main App component that sets up the root of the application.
2457
* It includes the GestureHandlerRootView for gesture handling,
@@ -70,16 +103,18 @@ const App = () => {
70103
<ConfigCatProvider
71104
sdkKey={__DEV__ ? CONFIGCAT_SDK_KEY_TEST : CONFIGCAT_SDK_KEY_PROD}>
72105
<Provider store={store}>
73-
<PersistGate loading={<Loading />} persistor={persistor}>
74-
<PaperProvider>
75-
<BannerProvider>
76-
<NotificationSubscriptionBootstrapper />
77-
<RootNavigation />
78-
<Toast />
79-
<ForceUpdate />
80-
</BannerProvider>
81-
</PaperProvider>
82-
</PersistGate>
106+
<MigrationGate>
107+
<PersistGate loading={<Loading />} persistor={persistor}>
108+
<PaperProvider>
109+
<BannerProvider>
110+
<NotificationSubscriptionBootstrapper />
111+
<RootNavigation />
112+
<Toast />
113+
<ForceUpdate />
114+
</BannerProvider>
115+
</PaperProvider>
116+
</PersistGate>
117+
</MigrationGate>
83118
</Provider>
84119
</ConfigCatProvider>
85120
</GestureHandlerRootView>

ios/Podfile.lock

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,11 +1515,60 @@ PODS:
15151515
- hermes-engine/Pre-built (= 0.76.9)
15161516
- hermes-engine/Pre-built (0.76.9)
15171517
- leveldb-library (1.22.6)
1518+
- MMKVCore (2.2.4)
15181519
- nanopb (3.30910.0):
15191520
- nanopb/decode (= 3.30910.0)
15201521
- nanopb/encode (= 3.30910.0)
15211522
- nanopb/decode (3.30910.0)
15221523
- nanopb/encode (3.30910.0)
1524+
- NitroMmkv (4.1.2):
1525+
- DoubleConversion
1526+
- glog
1527+
- hermes-engine
1528+
- MMKVCore (= 2.2.4)
1529+
- NitroModules
1530+
- RCT-Folly (= 2024.10.14.00)
1531+
- RCTRequired
1532+
- RCTTypeSafety
1533+
- React-callinvoker
1534+
- React-Core
1535+
- React-debug
1536+
- React-Fabric
1537+
- React-featureflags
1538+
- React-graphics
1539+
- React-ImageManager
1540+
- React-jsi
1541+
- React-NativeModulesApple
1542+
- React-RCTFabric
1543+
- React-rendererdebug
1544+
- React-utils
1545+
- ReactCodegen
1546+
- ReactCommon/turbomodule/bridging
1547+
- ReactCommon/turbomodule/core
1548+
- Yoga
1549+
- NitroModules (0.33.5):
1550+
- DoubleConversion
1551+
- glog
1552+
- hermes-engine
1553+
- RCT-Folly (= 2024.10.14.00)
1554+
- RCTRequired
1555+
- RCTTypeSafety
1556+
- React-callinvoker
1557+
- React-Core
1558+
- React-debug
1559+
- React-Fabric
1560+
- React-featureflags
1561+
- React-graphics
1562+
- React-ImageManager
1563+
- React-jsi
1564+
- React-NativeModulesApple
1565+
- React-RCTFabric
1566+
- React-rendererdebug
1567+
- React-utils
1568+
- ReactCodegen
1569+
- ReactCommon/turbomodule/bridging
1570+
- ReactCommon/turbomodule/core
1571+
- Yoga
15231572
- PromisesObjC (2.4.0)
15241573
- PromisesSwift (2.4.0):
15251574
- PromisesObjC (= 2.4.0)
@@ -3631,6 +3680,8 @@ DEPENDENCIES:
36313680
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
36323681
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
36333682
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
3683+
- NitroMmkv (from `../node_modules/react-native-mmkv`)
3684+
- NitroModules (from `../node_modules/react-native-nitro-modules`)
36343685
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
36353686
- RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
36363687
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
@@ -3755,6 +3806,7 @@ SPEC REPOS:
37553806
- GTMAppAuth
37563807
- GTMSessionFetcher
37573808
- leveldb-library
3809+
- MMKVCore
37583810
- nanopb
37593811
- PromisesObjC
37603812
- PromisesSwift
@@ -3786,6 +3838,10 @@ EXTERNAL SOURCES:
37863838
hermes-engine:
37873839
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
37883840
:tag: hermes-2024-11-12-RNv0.76.2-5b4aa20c719830dcf5684832b89a6edb95ac3d64
3841+
NitroMmkv:
3842+
:path: "../node_modules/react-native-mmkv"
3843+
NitroModules:
3844+
:path: "../node_modules/react-native-nitro-modules"
37893845
RCT-Folly:
37903846
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
37913847
RCTDeprecation:
@@ -4001,7 +4057,10 @@ SPEC CHECKSUMS:
40014057
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
40024058
hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11
40034059
leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19
4060+
MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df
40044061
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
4062+
NitroMmkv: 3582cd703e46cbb5bd2e466f134f73cb04690ddd
4063+
NitroModules: 8eedb6d7aba2c03eb1533c2caf4ebfad10cf551c
40054064
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
40064065
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
40074066
RCT-Folly: 86edbcc606c15fc0eced95db60b466fd5356e4b6

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
"react-native-gesture-handler": "^2.20.2",
4949
"react-native-google-mobile-ads": "^14.5.0",
5050
"react-native-linear-gradient": "^2.8.3",
51+
"react-native-mmkv": "^4.1.2",
52+
"react-native-nitro-modules": "^0.33.5",
5153
"react-native-orientation-locker": "^1.7.0",
5254
"react-native-paper": "^5.12.5",
5355
"react-native-permissions": "^5.2.1",

src/Redux/Storage/Storage.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1-
import AsyncStorage from '@react-native-async-storage/async-storage';
1+
import { MMKV } from 'react-native-mmkv';
2+
import {
3+
migrateAsyncStorageToMMKV,
4+
createMMKVAdapter,
5+
} from './migrateStorage';
26

3-
export const storage = AsyncStorage;
7+
// Create MMKV instance
8+
const mmkvStorage = new MMKV({
9+
id: 'inknest-redux-storage',
10+
// Optional: encryption for sensitive data
11+
// encryptionKey: 'your-encryption-key',
12+
});
13+
14+
// Export the adapter for redux-persist
15+
export const storage = createMMKVAdapter(mmkvStorage);
16+
17+
// Export MMKV instance for direct access if needed
18+
export { mmkvStorage };
19+
20+
// Export migration function to be called before store initialization
21+
export { migrateAsyncStorageToMMKV };
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* Storage Migration Utility
3+
* Migrates data from AsyncStorage to MMKV without data loss
4+
*/
5+
6+
import AsyncStorage from '@react-native-async-storage/async-storage';
7+
import { MMKV } from 'react-native-mmkv';
8+
9+
const MIGRATION_KEY = '@inknest_storage_migrated_v1';
10+
11+
/**
12+
* Check if migration has already been completed
13+
*/
14+
export async function isMigrationComplete() {
15+
const migrated = await AsyncStorage.getItem(MIGRATION_KEY);
16+
return migrated === 'true';
17+
}
18+
19+
/**
20+
* Mark migration as complete
21+
*/
22+
export async function markMigrationComplete() {
23+
await AsyncStorage.setItem(MIGRATION_KEY, 'true');
24+
}
25+
26+
/**
27+
* Get all keys from AsyncStorage that match redux-persist pattern
28+
*/
29+
async function getReduxPersistKeys() {
30+
const allKeys = await AsyncStorage.getAllKeys();
31+
// Redux-persist stores data with 'persist:' prefix
32+
return allKeys.filter(key => key.startsWith('persist:'));
33+
}
34+
35+
/**
36+
* Migrate a single key-value pair from AsyncStorage to MMKV
37+
*/
38+
async function migrateKeyToMMKV(key, mmkv) {
39+
try {
40+
const value = await AsyncStorage.getItem(key);
41+
if (value !== null) {
42+
mmkv.set(key, value);
43+
return true;
44+
}
45+
} catch (error) {
46+
console.error(`Failed to migrate key ${key}:`, error);
47+
}
48+
return false;
49+
}
50+
51+
/**
52+
* Main migration function
53+
* Call this BEFORE creating the MMKV storage instance for Redux
54+
* @returns {Promise<boolean>} - true if migration was successful or already done
55+
*/
56+
export async function migrateAsyncStorageToMMKV(mmkv) {
57+
try {
58+
// Check if already migrated
59+
const alreadyMigrated = await isMigrationComplete();
60+
if (alreadyMigrated) {
61+
return true;
62+
}
63+
64+
console.log('Starting AsyncStorage to MMKV migration...');
65+
66+
// Get all redux-persist keys
67+
const keysToMigrate = await getReduxPersistKeys();
68+
69+
if (keysToMigrate.length === 0) {
70+
console.log('No redux-persist data found in AsyncStorage');
71+
await markMigrationComplete();
72+
return true;
73+
}
74+
75+
console.log(`Found ${keysToMigrate.length} keys to migrate`);
76+
77+
// Migrate each key
78+
let migratedCount = 0;
79+
for (const key of keysToMigrate) {
80+
const success = await migrateKeyToMMKV(key, mmkv);
81+
if (success) {
82+
migratedCount++;
83+
}
84+
}
85+
86+
console.log(`Successfully migrated ${migratedCount}/${keysToMigrate.length} keys`);
87+
88+
// Mark migration as complete
89+
await markMigrationComplete();
90+
91+
return true;
92+
} catch (error) {
93+
console.error('Storage migration failed:', error);
94+
// Don't mark as complete if migration failed
95+
// This allows retry on next app launch
96+
return false;
97+
}
98+
}
99+
100+
/**
101+
* Fallback: If MMKV fails, use AsyncStorage adapter
102+
*/
103+
export function createMMKVAdapter(mmkv) {
104+
return {
105+
setItem: (key, value) => {
106+
mmkv.set(key, value);
107+
return Promise.resolve();
108+
},
109+
getItem: (key) => {
110+
const value = mmkv.getString(key);
111+
return Promise.resolve(value ?? null);
112+
},
113+
removeItem: (key) => {
114+
mmkv.delete(key);
115+
return Promise.resolve();
116+
},
117+
};
118+
}

src/Redux/Store/index.js

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
import {configureStore} from '@reduxjs/toolkit';
22
import {persistStore, persistReducer} from 'redux-persist';
3-
import {storage} from '../Storage/Storage';
3+
import {storage, migrateAsyncStorageToMMKV, mmkvStorage} from '../Storage/Storage';
44
import Reducers from '../Reducers';
5+
import crashlytics from '@react-native-firebase/crashlytics';
56

67
const persistConfig = {
78
key: 'root',
9+
version: 1, // Add version for future state migrations
810
storage: storage,
911
blacklist: ['error', 'status', 'loading', 'downTime', 'hasRewardAdsShown'],
12+
// Add state reconciler to handle version changes
13+
stateReconciler: (inboundState, originalState) => {
14+
// If inbound state is invalid/corrupted, return original state
15+
if (!inboundState || typeof inboundState !== 'object') {
16+
return originalState;
17+
}
18+
return inboundState;
19+
},
1020
};
1121

1222
const persistedReducer = persistReducer(persistConfig, Reducers);
@@ -31,4 +41,37 @@ const store = configureStore({
3141

3242
const persistor = persistStore(store);
3343

44+
/**
45+
* Perform storage migration from AsyncStorage to MMKV
46+
* Call this function ONCE during app initialization (before rendering)
47+
* @returns {Promise<boolean>}
48+
*/
49+
export async function performStorageMigration() {
50+
try {
51+
const success = await migrateAsyncStorageToMMKV(mmkvStorage);
52+
if (success) {
53+
// After successful migration, purge and rehydrate to load migrated data
54+
await persistor.purge();
55+
await persistor.persist();
56+
}
57+
return success;
58+
} catch (error) {
59+
console.error('Migration error:', error);
60+
crashlytics().recordError(error);
61+
return false;
62+
}
63+
}
64+
65+
/**
66+
* Get storage size info for debugging/monitoring
67+
*/
68+
export function getStorageInfo() {
69+
const size = mmkvStorage.getSize();
70+
return {
71+
size: size,
72+
// MMKV has no practical limit (depends on device storage)
73+
isHealthy: size > 0 || size === 0, // Size 0 means empty, which is fine
74+
};
75+
}
76+
3477
export {store, persistor};

0 commit comments

Comments
 (0)