Background: I've tried reading up on this issue, but can't come up with an explanation of what actually happens on iOS devices w.r.t. a react-redux and redux-persist store setup.
References:
- How to reset the state of a Redux store?
- https://blog.bam.tech/developer-news/redux-persist-how-it-works-and-how-to-change-the-structure-of-your-persisted-store
- https://discussions.apple.com/thread/250190465
Problem:
- Our QA team was unable to test out the onboarding experience.
- Onboarding experience: 3 intro screens (controlled by
isNew
flag -> login -> onboarding tutorial (triggered byis_first_time_login: true
,hasLoggedIn || hasPinCodeLoggedIn: true
, andhasFinishedOnboardTutorial:false
- We've tried on both Android and iOS devices, Android works as intended, but for iOS, it doesn't.
- So for iOS devices, we've deleted the app, restarted the phone, then installed the test app. The test app is distributed via firebase distribution.
- The problem persists, and I'm at a loss on what to do now.
Code setup:
store/index.js
import { applyMiddleware, compose, createStore } from 'redux';
import { persistReducer, persistStore, createTransform } from 'redux-persist';
import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger';
import createSensitiveStorage from 'redux-persist-sensitive-storage';
import { NODE_ENV } from '@env';
import isEmpty from 'lodash/isEmpty';
import RootSaga from 'Sagas';
import RootReducer from 'Reducers';
import { UserDetail } from 'Models';
import ApiUtils from 'Services';
import { Configs, reactotron } from 'Configs';
const secureStorage = createSensitiveStorage({
keychainService: 'xxxx',
sharedPreferencesName: 'xxx'
});
const PersistStateTransformer = createTransform(
(inboundState) => {
return { ...inboundState };
},
(outboundState) => {
if (!isEmpty(outboundState?.token)) {
ApiUtils.injectToken(outboundState?.token);
}
return { ...outboundState, userDetails: new UserDetail(outboundState.userDetails), backgroundTimestamp: 0, hasPinCodeLoggedIn: false };
},
{
whitelist: ['auth'],
}
);
const persistConfig = {
key: 'root',
storage: secureStorage,
transforms: [PersistStateTransformer],
whitelist: ['auth', 'ui'],
};
// Use to remove all the existing state including auth/ui
// if (NODE_ENV === 'development') {
// AsyncStorage.removeItem('persist:root');
// }
export const buildStore = (initialState = {}) => {
const middlewares = [];
const enhancers = [];
// Connect the sagas to the redux store
const sagaMiddleware = createSagaMiddleware();
middlewares.push(sagaMiddleware);
if (NODE_ENV === 'development') {
middlewares.push(logger);
}
// do not remove spread operator
enhancers.push(applyMiddleware(...middlewares));
// Redux persist
const persistedReducer = persistReducer(persistConfig, RootReducer);
// Reactotron is only needed during dev, in test env it's not necessary
const store =
NODE_ENV === 'development'
? createStore(persistedReducer, initialState, compose(...enhancers, reactotron.createEnhancer()))
: createStore(persistedReducer, initialState, compose(...enhancers));
const persistor = persistStore(store);
// Kick off the root saga
sagaMiddleware.run(RootSaga);
return { store, persistor };
};
App.js
import React, { useEffect } from 'react';
import { Platform, UIManager } from 'react-native';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import SplashScreen from 'react-native-splash-screen';
import Navigation from 'Navigations';
import { buildStore } from 'Stores';
import { Loading } from 'Screens';
import { Toast } from 'Components';
import GlobalUiProvider from 'Contexts';
import ApiUtils from 'Services';
const { store, persistor } = buildStore();
ApiUtils.injectStore(store);
const App = () => {
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
}
useEffect(() => {
SplashScreen.hide();
}, []);
return (
<SafeAreaProvider>
<Provider store={store}>
<PersistGate loading={<Loading />} persistor={persistor}>
<GlobalUiProvider>
<Navigation />
</GlobalUiProvider>
<Toast />
</PersistGate>
</Provider>
</SafeAreaProvider>
);
};
export default App;
export { store, persistor };
publicNavigation.js (snippet)
<PublicStack.Navigator screenOptions={Configs.PUBLIC_SCREEN_OPTIONS}>
{isNew ? (
<PublicStack.Screen name="Onboard" component={Onboard} />
) : (
<PublicStack.Screen name="Login" component={Login} />
)}
</PublicStack.Navigator>
);
Let me know if I need to add more details.