├── .gitignore
├── README.md
├── babel.config.js
├── dist
├── createAsyncStorage.js
├── createMigrate.js
├── createPersistContext.js
├── createSensitiveStorage.js
├── defaultOptions.js
├── index.js
├── usePersistStorage.js
└── utils.js
├── package.json
├── src
├── createAsyncStorage.ts
├── createMigrate.ts
├── createPersistContext.tsx
├── createSensitiveStorage.ts
├── defaultOptions.ts
├── index.ts
├── usePersistStorage.ts
└── utils.ts
├── tests
├── __mocks__
│ ├── @react-native-community
│ │ └── async-storage.js
│ └── react-native-sensitive-info.js
├── createAsyncStorage.test.ts
├── createMigrate.test.ts
├── setup.ts
├── usePersistStorage.test.ts
└── utils.ts
├── tsconfig.json
├── tslint.json
├── types
├── createAsyncStorage.d.ts
├── createMigrate.d.ts
├── createPersistContext.d.ts
├── createSensitiveStorage.d.ts
├── defaultOptions.d.ts
├── index.d.ts
├── usePersistStorage.d.ts
└── utils.d.ts
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | .DS_Store
9 | .env
10 | build
11 | node_modules
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native Hooks - usePersistStorage
2 |
3 | Persist and rehydrate a **context store** by React Hooks
4 |
5 | - An asynchronous persist storage
6 | - Support **sensitive info** both on iOS & Android
7 | - Migration function
8 |
9 | [](https://badge.fury.io/js/react-native-use-persist-storage)
10 |
11 | ## Install
12 |
13 | Install react-native-use-persist-storage
14 |
15 | ```
16 | $ yarn add react-native-use-persist-storage
17 | ```
18 |
19 | Install **@react-native-async-storage/async-storage**, **react-native-sensitive-info**
20 | (see [peerDependencies](https://github.com/visuallylab/react-native-use-persist-storage#peer-dependencies))
21 |
22 | ```
23 | $ yarn add react-native-sensitive-info @react-native-async-storage/async-storage
24 | ```
25 |
26 | If RN < 0.60, link dependencies
27 |
28 | ```
29 | $ react-native link react-native-sensitive-info
30 | $ react-native link @react-native-async-storage/async-storage
31 | ```
32 |
33 | Note: For IOS project using pods, run: \$ cd ios/ && pod install
34 |
35 | #### Peer Dependencies
36 |
37 | **Note:** `react-native-use-persist-storage` requires the following peer dependencies:
38 |
39 | - react >= 16.8.1
40 | - react-native >= 0.59.0
41 | - [react-native-sensitive-info](https://github.com/mCodex/react-native-sensitive-info)
42 | - [@react-native-async-storage/async-storage](https://github.com/react-native-async-storage/async-storage)
43 |
44 | ---
45 |
46 | ## Usage
47 |
48 | Make sure you know how [React Hooks](https://reactjs.org/docs/hooks-reference.html) works, and you can use it just same as useState.
49 |
50 | #### Basic Usage
51 |
52 | ```js
53 | // User.js
54 | import { usePersistStorage } from "react-native-use-persist-storage";
55 |
56 | const createDefaultUser = () => ({
57 | name: ""
58 | });
59 |
60 | const User = props => {
61 | const [user, setUser, restored] = usePersistStorage(
62 | "@User",
63 | createDefaultUser
64 | );
65 | if (restored) return loading...;
66 | return {user.name};
67 | };
68 | ```
69 |
70 | #### Context Usage
71 |
72 | If you want a light-weight global state management solution in react, try using [Context](https://reactjs.org/docs/context.html).
73 |
74 | ```js
75 | // contexts/user.js
76 | import { createContext } from 'react'
77 | import { usePersistStorage } from 'react-native-use-persist-storage'
78 |
79 | const createDefaultUser = () => ({
80 | name: '',
81 | });
82 |
83 | const UserContext = createContext(...);
84 |
85 | const UserProvider = props => {
86 | const [user, setUser, restored] = usePersistStorage('@User', createDefaultUser);
87 |
88 | // anything you need...
89 |
90 | return (
91 |
96 | {props.children}
97 |
98 | );
99 | };
100 | ```
101 |
102 | ```js
103 | // GlobalStateProvider.ts
104 | const GlobalStateProvider = ({ children }) => (
105 |
106 |
107 | {children}
108 |
109 |
110 | )
111 |
112 | // App.js
113 | const App = () => {
114 | return (
115 |
116 |
117 |
118 | );
119 | };
120 | ```
121 |
122 | #### Recommend use: [createPersistContext](#createPersistContext)
123 |
124 | ## API
125 |
126 | #### `usePersistStorage(key, initialValue, options?)`
127 |
128 | - arguments
129 | - `key`: async storage key
130 | - `initialValue`: initial value
131 | - `options`: set options `debug`, `persist`, `version`, `migrate`, `sensitive`.
132 |
133 | ##### `options` ([see](https://github.com/visuallylab/react-native-use-persist-storage/blob/master/src/defaultOptions.ts#L4))
134 |
135 | - `debug`: call `console.debug` when any persist storage action.
136 | - default: `false`
137 | - `persist`: set false, it will same as useState
138 | - default: `true`
139 | - `version`: storage version, set with migrate function
140 | - default: `0`
141 | - `migrate`: set migrate function, [see how to use createMigrate](#createMigrate)
142 | - default: `null`
143 | - `sensitive`: pass [config options](https://mcodex.dev/react-native-sensitive-info/docs/5.x/ios_options), it will use [react-native-sensitive-info](https://github.com/mCodex/react-native-sensitive-info) to store your data.
144 | - default: `false`
145 |
146 | #### `createPersistContext`
147 |
148 | It is a simple built-in util function for easy use. [See](https://github.com/visuallylab/react-native-use-persist-storage/blob/master/src/createPersistContext.tsx)
149 |
150 | ```js
151 | // contexts/user.js
152 | import { createPersistContext } from 'react-native-use-persist-storage';
153 |
154 | export default createPersistContext({
155 | storageKey: '@User',
156 | defaultData: {
157 | name: 'Awesome!'
158 | },
159 | options: { ... }
160 | });
161 |
162 | // App.js
163 | import User from './contexts/user';
164 | const App = () => {
165 | return (
166 |
167 |
168 |
169 | );
170 | };
171 |
172 | // Component.js
173 | import User from './contexts/user';
174 |
175 | const Component = () => {
176 | const [user, setUser] = User.useData();
177 | return ...
178 | };
179 |
180 | ```
181 |
182 | **Or, you can create a hook to customize**
183 |
184 | ```js
185 | // hooks/useUser.js
186 | import User from './contexts/user';
187 |
188 | const useUser = () => {
189 | const [user, setUser] = useContext(User.Context);
190 | const setName = () => ...
191 | const setEmail = () => ...
192 | return {
193 | user,
194 | setName,
195 | setEmail,
196 | setUser,
197 | // anything ...
198 | };
199 | };
200 |
201 | const Component = () => {
202 | const { user, ... } = useUser();
203 | ...
204 | }
205 |
206 | ```
207 |
208 | #### `createMigrate`
209 |
210 | Use like [redux migration](https://github.com/rt2zz/redux-persist/blob/master/docs/migrations.md)
211 |
212 | ```js
213 | import { createMigrate, usePersistStorage } from 'react-native-use-persist-storage';
214 |
215 | const [user, setUser, restored] = usePersistStorage(
216 | '@User',
217 | defaultUser,
218 | {
219 | version: 2,
220 | migrate: createMigrate(
221 | {
222 | 1: state => {
223 | // ...
224 | },
225 | 2: state => ({
226 | // ...
227 | }),
228 |
229 | },
230 | ),
231 | },
232 | )
233 |
234 | ```
235 |
236 | #### `setPersistStorageDefaults`
237 | You can control all default options when app initialized.([see](https://github.com/visuallylab/react-native-use-persist-storage/blob/master/src/defaultOptions.ts#L4))
238 |
239 | - open debug log:
240 | ```js
241 | setPersistStorageDefaults({ debug: true });
242 | ```
243 |
244 | - remove all stored values in persistent storage:
245 | ```js
246 | setPersistStorageDefaults({ persist: false });
247 | ```
248 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | // babel.config.js
2 | module.exports = {
3 | presets: [
4 | [
5 | "@babel/preset-env",
6 | {
7 | targets: {
8 | node: "current"
9 | }
10 | }
11 | ],
12 | "@babel/preset-typescript"
13 | ],
14 | "plugins": ["transform-flow-strip-types"]
15 | };
16 |
--------------------------------------------------------------------------------
/dist/createAsyncStorage.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 | return new (P || (P = Promise))(function (resolve, reject) {
5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 | step((generator = generator.apply(thisArg, _arguments || [])).next());
9 | });
10 | };
11 | var __generator = (this && this.__generator) || function (thisArg, body) {
12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14 | function verb(n) { return function (v) { return step([n, v]); }; }
15 | function step(op) {
16 | if (f) throw new TypeError("Generator is already executing.");
17 | while (_) try {
18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19 | if (y = 0, t) op = [op[0] & 2, t.value];
20 | switch (op[0]) {
21 | case 0: case 1: t = op; break;
22 | case 4: _.label++; return { value: op[1], done: false };
23 | case 5: _.label++; y = op[1]; op = [0]; continue;
24 | case 7: op = _.ops.pop(); _.trys.pop(); continue;
25 | default:
26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30 | if (t[2]) _.ops.pop();
31 | _.trys.pop(); continue;
32 | }
33 | op = body.call(thisArg, _);
34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36 | }
37 | };
38 | var __importDefault = (this && this.__importDefault) || function (mod) {
39 | return (mod && mod.__esModule) ? mod : { "default": mod };
40 | };
41 | Object.defineProperty(exports, "__esModule", { value: true });
42 | var async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
43 | var noop = function () { return null; };
44 | var createAsyncStorage = function () {
45 | return {
46 | getItem: function (key, callback) {
47 | if (callback === void 0) { callback = noop; }
48 | return __awaiter(this, void 0, void 0, function () {
49 | var result, error_1;
50 | return __generator(this, function (_a) {
51 | switch (_a.label) {
52 | case 0:
53 | _a.trys.push([0, 2, , 3]);
54 | return [4, async_storage_1.default.getItem(key)];
55 | case 1:
56 | result = _a.sent();
57 | callback(null, result);
58 | return [2, result];
59 | case 2:
60 | error_1 = _a.sent();
61 | callback(error_1, null);
62 | throw error_1;
63 | case 3: return [2];
64 | }
65 | });
66 | });
67 | },
68 | setItem: function (key, value, callback) {
69 | if (callback === void 0) { callback = noop; }
70 | return __awaiter(this, void 0, void 0, function () {
71 | var error_2;
72 | return __generator(this, function (_a) {
73 | switch (_a.label) {
74 | case 0:
75 | _a.trys.push([0, 2, , 3]);
76 | return [4, async_storage_1.default.setItem(key, value)];
77 | case 1:
78 | _a.sent();
79 | callback(null, value);
80 | return [3, 3];
81 | case 2:
82 | error_2 = _a.sent();
83 | callback(error_2, null);
84 | throw error_2;
85 | case 3: return [2];
86 | }
87 | });
88 | });
89 | },
90 | removeItem: function (key, callback) {
91 | if (callback === void 0) { callback = noop; }
92 | return __awaiter(this, void 0, void 0, function () {
93 | var error_3;
94 | return __generator(this, function (_a) {
95 | switch (_a.label) {
96 | case 0:
97 | _a.trys.push([0, 2, , 3]);
98 | return [4, async_storage_1.default.removeItem(key)];
99 | case 1:
100 | _a.sent();
101 | callback(null, null);
102 | return [3, 3];
103 | case 2:
104 | error_3 = _a.sent();
105 | callback(error_3, null);
106 | throw error_3;
107 | case 3: return [2];
108 | }
109 | });
110 | });
111 | },
112 | };
113 | };
114 | exports.default = createAsyncStorage;
115 |
--------------------------------------------------------------------------------
/dist/createMigrate.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | var defaultOptions_1 = require("./defaultOptions");
4 | var utils_1 = require("./utils");
5 | var createMigrate = function (migrations, configs) {
6 | if (configs === void 0) { configs = { debug: defaultOptions_1.defaultOptions.debug }; }
7 | return function (_a) {
8 | var key = _a.key, state = _a.state, version = _a.version;
9 | var debug = configs.debug;
10 | if (!state.value) {
11 | if (debug) {
12 | console.debug("[" + key + "]: no inbound value, skipping migration");
13 | }
14 | return state;
15 | }
16 | if (state._currentVersion === version) {
17 | if (debug) {
18 | console.debug("[" + key + "]: version match, no migration");
19 | }
20 | return state;
21 | }
22 | if (state._currentVersion > version) {
23 | console.warn("[" + key + "]: downgrading version is not supported");
24 | return state;
25 | }
26 | var migrationKeys = Object.keys(migrations)
27 | .map(function (v) { return parseInt(v, 10); })
28 | .filter(function (ver) { return version >= ver && ver > state._currentVersion; })
29 | .sort(function (a, b) { return a - b; });
30 | if (debug) {
31 | console.debug("[" + key + "]: migration keys", migrationKeys);
32 | }
33 | var migrated = migrationKeys.reduce(function (_a, versionKey) {
34 | var value = _a.value;
35 | if (debug) {
36 | console.warn("[" + key + "]: running migration " + versionKey);
37 | }
38 | return utils_1.transformStorageValue(migrations[versionKey](value), versionKey);
39 | }, state);
40 | return migrated;
41 | };
42 | };
43 | exports.default = createMigrate;
44 |
--------------------------------------------------------------------------------
/dist/createPersistContext.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __assign = (this && this.__assign) || function () {
3 | __assign = Object.assign || function(t) {
4 | for (var s, i = 1, n = arguments.length; i < n; i++) {
5 | s = arguments[i];
6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7 | t[p] = s[p];
8 | }
9 | return t;
10 | };
11 | return __assign.apply(this, arguments);
12 | };
13 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
14 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
15 | return new (P || (P = Promise))(function (resolve, reject) {
16 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
17 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
18 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
19 | step((generator = generator.apply(thisArg, _arguments || [])).next());
20 | });
21 | };
22 | var __generator = (this && this.__generator) || function (thisArg, body) {
23 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
24 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25 | function verb(n) { return function (v) { return step([n, v]); }; }
26 | function step(op) {
27 | if (f) throw new TypeError("Generator is already executing.");
28 | while (_) try {
29 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30 | if (y = 0, t) op = [op[0] & 2, t.value];
31 | switch (op[0]) {
32 | case 0: case 1: t = op; break;
33 | case 4: _.label++; return { value: op[1], done: false };
34 | case 5: _.label++; y = op[1]; op = [0]; continue;
35 | case 7: op = _.ops.pop(); _.trys.pop(); continue;
36 | default:
37 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41 | if (t[2]) _.ops.pop();
42 | _.trys.pop(); continue;
43 | }
44 | op = body.call(thisArg, _);
45 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47 | }
48 | };
49 | var __importDefault = (this && this.__importDefault) || function (mod) {
50 | return (mod && mod.__esModule) ? mod : { "default": mod };
51 | };
52 | Object.defineProperty(exports, "__esModule", { value: true });
53 | var react_1 = __importDefault(require("react"));
54 | var usePersistStorage_1 = __importDefault(require("./usePersistStorage"));
55 | var createPersistContext = function (_a) {
56 | var storageKey = _a.storageKey, defaultData = _a.defaultData, options = _a.options;
57 | var createDefaultData = function () { return defaultData; };
58 | var Context = react_1.default.createContext([
59 | createDefaultData(),
60 | function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
61 | ;
62 | return [2];
63 | }); }); },
64 | false
65 | ]);
66 | var Provider = function (props) {
67 | var _a = usePersistStorage_1.default(storageKey, createDefaultData, __assign({ persist: props.persist }, options)), data = _a[0], setData = _a[1], restored = _a[2];
68 | return (react_1.default.createElement(Context.Provider, { value: [data, setData, restored] }, props.children));
69 | };
70 | var useData = function () {
71 | var context = react_1.default.useContext(Context);
72 | if (!context) {
73 | throw new Error("Context error: context [" + storageKey + "] must be used within a Provider");
74 | }
75 | return context;
76 | };
77 | return {
78 | Provider: Provider,
79 | Context: Context,
80 | useData: useData
81 | };
82 | };
83 | exports.default = createPersistContext;
84 |
--------------------------------------------------------------------------------
/dist/createSensitiveStorage.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 | return new (P || (P = Promise))(function (resolve, reject) {
5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 | step((generator = generator.apply(thisArg, _arguments || [])).next());
9 | });
10 | };
11 | var __generator = (this && this.__generator) || function (thisArg, body) {
12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14 | function verb(n) { return function (v) { return step([n, v]); }; }
15 | function step(op) {
16 | if (f) throw new TypeError("Generator is already executing.");
17 | while (_) try {
18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19 | if (y = 0, t) op = [op[0] & 2, t.value];
20 | switch (op[0]) {
21 | case 0: case 1: t = op; break;
22 | case 4: _.label++; return { value: op[1], done: false };
23 | case 5: _.label++; y = op[1]; op = [0]; continue;
24 | case 7: op = _.ops.pop(); _.trys.pop(); continue;
25 | default:
26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30 | if (t[2]) _.ops.pop();
31 | _.trys.pop(); continue;
32 | }
33 | op = body.call(thisArg, _);
34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36 | }
37 | };
38 | var __importDefault = (this && this.__importDefault) || function (mod) {
39 | return (mod && mod.__esModule) ? mod : { "default": mod };
40 | };
41 | Object.defineProperty(exports, "__esModule", { value: true });
42 | var react_native_1 = require("react-native");
43 | var react_native_sensitive_info_1 = __importDefault(require("react-native-sensitive-info"));
44 | var extractKeys = react_native_1.Platform.select({
45 | ios: function (items) { return items[0].map(function (item) { return item.key; }); },
46 | android: Object.keys,
47 | });
48 | var noop = function () { return null; };
49 | var createSensitiveStorage = function (sensitiveOpts) {
50 | if (sensitiveOpts === void 0) { sensitiveOpts = {}; }
51 | return {
52 | getItem: function (key, callback) {
53 | if (callback === void 0) { callback = noop; }
54 | return __awaiter(this, void 0, void 0, function () {
55 | var result, error_1;
56 | return __generator(this, function (_a) {
57 | switch (_a.label) {
58 | case 0:
59 | _a.trys.push([0, 2, , 3]);
60 | return [4, react_native_sensitive_info_1.default.getItem(key, sensitiveOpts)];
61 | case 1:
62 | result = _a.sent();
63 | if (typeof result === 'undefined') {
64 | result = null;
65 | }
66 | callback(null, result);
67 | return [2, result];
68 | case 2:
69 | error_1 = _a.sent();
70 | callback(error_1, null);
71 | throw error_1;
72 | case 3: return [2];
73 | }
74 | });
75 | });
76 | },
77 | setItem: function (key, value, callback) {
78 | if (callback === void 0) { callback = noop; }
79 | return __awaiter(this, void 0, void 0, function () {
80 | var error_2;
81 | return __generator(this, function (_a) {
82 | switch (_a.label) {
83 | case 0:
84 | _a.trys.push([0, 2, , 3]);
85 | return [4, react_native_sensitive_info_1.default.setItem(key, value, sensitiveOpts)];
86 | case 1:
87 | _a.sent();
88 | callback(null, value);
89 | return [3, 3];
90 | case 2:
91 | error_2 = _a.sent();
92 | callback(error_2, null);
93 | throw error_2;
94 | case 3: return [2];
95 | }
96 | });
97 | });
98 | },
99 | removeItem: function (key, callback) {
100 | if (callback === void 0) { callback = noop; }
101 | return __awaiter(this, void 0, void 0, function () {
102 | var error_3;
103 | return __generator(this, function (_a) {
104 | switch (_a.label) {
105 | case 0:
106 | _a.trys.push([0, 2, , 3]);
107 | return [4, react_native_sensitive_info_1.default.deleteItem(key, sensitiveOpts)];
108 | case 1:
109 | _a.sent();
110 | callback(null, null);
111 | return [3, 3];
112 | case 2:
113 | error_3 = _a.sent();
114 | callback(error_3, null);
115 | throw error_3;
116 | case 3: return [2];
117 | }
118 | });
119 | });
120 | },
121 | getAllKeys: function (callback) {
122 | if (callback === void 0) { callback = noop; }
123 | return __awaiter(this, void 0, void 0, function () {
124 | var values, result, error_4;
125 | return __generator(this, function (_a) {
126 | switch (_a.label) {
127 | case 0:
128 | _a.trys.push([0, 2, , 3]);
129 | return [4, react_native_sensitive_info_1.default.getAllItems(sensitiveOpts)];
130 | case 1:
131 | values = (_a.sent());
132 | result = extractKeys(values);
133 | callback(null, result);
134 | return [2, result];
135 | case 2:
136 | error_4 = _a.sent();
137 | callback(error_4, null);
138 | throw error_4;
139 | case 3: return [2];
140 | }
141 | });
142 | });
143 | },
144 | };
145 | };
146 | exports.default = createSensitiveStorage;
147 |
--------------------------------------------------------------------------------
/dist/defaultOptions.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __assign = (this && this.__assign) || function () {
3 | __assign = Object.assign || function(t) {
4 | for (var s, i = 1, n = arguments.length; i < n; i++) {
5 | s = arguments[i];
6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7 | t[p] = s[p];
8 | }
9 | return t;
10 | };
11 | return __assign.apply(this, arguments);
12 | };
13 | Object.defineProperty(exports, "__esModule", { value: true });
14 | exports.defaultOptions = {
15 | debug: false,
16 | version: 0,
17 | persist: true,
18 | migrate: null,
19 | sensitive: false,
20 | };
21 | exports.setPersistStorageDefaults = function (configs) {
22 | exports.defaultOptions = __assign(__assign({}, exports.defaultOptions), configs);
23 | };
24 |
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | function __export(m) {
3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
4 | }
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | __export(require("./usePersistStorage"));
7 | var usePersistStorage_1 = require("./usePersistStorage");
8 | exports.usePersistStorage = usePersistStorage_1.default;
9 | __export(require("./createPersistContext"));
10 | var createPersistContext_1 = require("./createPersistContext");
11 | exports.createPersistContext = createPersistContext_1.default;
12 | __export(require("./createMigrate"));
13 | var createMigrate_1 = require("./createMigrate");
14 | exports.createMigrate = createMigrate_1.default;
15 | var defaultOptions_1 = require("./defaultOptions");
16 | exports.setPersistStorageDefaults = defaultOptions_1.setPersistStorageDefaults;
17 |
--------------------------------------------------------------------------------
/dist/usePersistStorage.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 | return new (P || (P = Promise))(function (resolve, reject) {
5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 | step((generator = generator.apply(thisArg, _arguments || [])).next());
9 | });
10 | };
11 | var __generator = (this && this.__generator) || function (thisArg, body) {
12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14 | function verb(n) { return function (v) { return step([n, v]); }; }
15 | function step(op) {
16 | if (f) throw new TypeError("Generator is already executing.");
17 | while (_) try {
18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19 | if (y = 0, t) op = [op[0] & 2, t.value];
20 | switch (op[0]) {
21 | case 0: case 1: t = op; break;
22 | case 4: _.label++; return { value: op[1], done: false };
23 | case 5: _.label++; y = op[1]; op = [0]; continue;
24 | case 7: op = _.ops.pop(); _.trys.pop(); continue;
25 | default:
26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30 | if (t[2]) _.ops.pop();
31 | _.trys.pop(); continue;
32 | }
33 | op = body.call(thisArg, _);
34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36 | }
37 | };
38 | var __importDefault = (this && this.__importDefault) || function (mod) {
39 | return (mod && mod.__esModule) ? mod : { "default": mod };
40 | };
41 | Object.defineProperty(exports, "__esModule", { value: true });
42 | var react_1 = require("react");
43 | var createAsyncStorage_1 = __importDefault(require("./createAsyncStorage"));
44 | var createSensitiveStorage_1 = __importDefault(require("./createSensitiveStorage"));
45 | var defaultOptions_1 = require("./defaultOptions");
46 | var utils_1 = require("./utils");
47 | var usePersistStorage = function (key, initialValue, _a) {
48 | var _b = _a === void 0 ? defaultOptions_1.defaultOptions : _a, _c = _b.debug, debug = _c === void 0 ? defaultOptions_1.defaultOptions.debug : _c, _d = _b.version, version = _d === void 0 ? defaultOptions_1.defaultOptions.version : _d, _e = _b.persist, persist = _e === void 0 ? defaultOptions_1.defaultOptions.persist : _e, _f = _b.migrate, migrate = _f === void 0 ? defaultOptions_1.defaultOptions.migrate : _f, _g = _b.sensitive, sensitive = _g === void 0 ? defaultOptions_1.defaultOptions.sensitive : _g;
49 | var currentVersion = react_1.useRef(version || 0);
50 | var _h = react_1.useState(initialValue), state = _h[0], setState = _h[1];
51 | var _j = react_1.useState(false), restored = _j[0], setRestored = _j[1];
52 | var Storage = react_1.useMemo(function () {
53 | return sensitive ? createSensitiveStorage_1.default(sensitive) : createAsyncStorage_1.default();
54 | }, [sensitive]);
55 | var logPrefix = sensitive ? "(sensitive)" : "";
56 | var setValueToStorage = react_1.useCallback(function (newValue) { return __awaiter(void 0, void 0, void 0, function () {
57 | var value, serializedValue, err_1;
58 | return __generator(this, function (_a) {
59 | switch (_a.label) {
60 | case 0:
61 | value = utils_1.transformStorageValue(newValue, currentVersion.current);
62 | _a.label = 1;
63 | case 1:
64 | _a.trys.push([1, 3, , 4]);
65 | serializedValue = JSON.stringify(value);
66 | return [4, Storage.setItem(key, serializedValue)];
67 | case 2:
68 | _a.sent();
69 | if (debug) {
70 | console.debug(logPrefix + "[PersistStorage]: set " + key + ": ", value);
71 | }
72 | return [3, 4];
73 | case 3:
74 | err_1 = _a.sent();
75 | console.error(err_1);
76 | return [3, 4];
77 | case 4: return [2];
78 | }
79 | });
80 | }); }, [Storage]);
81 | react_1.useEffect(function () {
82 | if (persist) {
83 | var restoreStateFromStorage = function () { return __awaiter(void 0, void 0, void 0, function () {
84 | var storageValue, parsedValue, err_2;
85 | return __generator(this, function (_a) {
86 | switch (_a.label) {
87 | case 0:
88 | _a.trys.push([0, 7, , 8]);
89 | return [4, Storage.getItem(key)];
90 | case 1:
91 | storageValue = _a.sent();
92 | if (!storageValue) return [3, 4];
93 | parsedValue = JSON.parse(storageValue || "null");
94 | if (parsedValue && parsedValue._currentVersion === undefined) {
95 | parsedValue = utils_1.transformStorageValue(parsedValue, currentVersion.current);
96 | }
97 | if (!migrate) return [3, 3];
98 | parsedValue = migrate({
99 | key: key,
100 | state: parsedValue,
101 | version: currentVersion.current
102 | });
103 | currentVersion.current = parsedValue._currentVersion;
104 | return [4, setValueToStorage(parsedValue.value)];
105 | case 2:
106 | _a.sent();
107 | _a.label = 3;
108 | case 3:
109 | setState(parsedValue.value);
110 | if (debug) {
111 | console.debug(logPrefix + "[PersistStorage]: restore " + key + ": ", parsedValue);
112 | }
113 | return [3, 6];
114 | case 4: return [4, setValueToStorage(state)];
115 | case 5:
116 | _a.sent();
117 | _a.label = 6;
118 | case 6: return [3, 8];
119 | case 7:
120 | err_2 = _a.sent();
121 | console.error(err_2);
122 | return [3, 8];
123 | case 8:
124 | setRestored(true);
125 | return [2];
126 | }
127 | });
128 | }); };
129 | restoreStateFromStorage();
130 | }
131 | else {
132 | var removePersistItem = function () { return __awaiter(void 0, void 0, void 0, function () {
133 | return __generator(this, function (_a) {
134 | switch (_a.label) {
135 | case 0: return [4, Storage.removeItem(key)];
136 | case 1:
137 | _a.sent();
138 | setRestored(true);
139 | return [2];
140 | }
141 | });
142 | }); };
143 | removePersistItem();
144 | if (debug) {
145 | console.debug(logPrefix + "[PersistStorage]: remove " + key);
146 | }
147 | }
148 | }, []);
149 | var asyncSetState = function (stateOrCallbackFn) { return __awaiter(void 0, void 0, void 0, function () {
150 | var newValue;
151 | return __generator(this, function (_a) {
152 | switch (_a.label) {
153 | case 0:
154 | newValue = stateOrCallbackFn instanceof Function
155 | ? stateOrCallbackFn(state)
156 | : stateOrCallbackFn;
157 | setState(newValue);
158 | if (!persist) return [3, 2];
159 | return [4, setValueToStorage(newValue)];
160 | case 1:
161 | _a.sent();
162 | _a.label = 2;
163 | case 2: return [2];
164 | }
165 | });
166 | }); };
167 | return [state, asyncSetState, restored];
168 | };
169 | exports.default = usePersistStorage;
170 |
--------------------------------------------------------------------------------
/dist/utils.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | function transformStorageValue(value, version) {
4 | if (version === void 0) { version = 0; }
5 | return {
6 | _currentVersion: version,
7 | value: value
8 | };
9 | }
10 | exports.transformStorageValue = transformStorageValue;
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-use-persist-storage",
3 | "version": "1.0.2",
4 | "main": "dist/index.js",
5 | "types": "types/index.d.ts",
6 | "description": "Persist and rehydrate a context store by React Hooks",
7 | "scripts": {
8 | "test": "jest --runInBand",
9 | "build": "tsc --pretty --skipLibCheck",
10 | "prepublishOnly": "npm run build"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/visuallylab/react-native-usePersistStorage.git"
15 | },
16 | "keywords": [
17 | "react-native",
18 | "hooks",
19 | "async-storage",
20 | "sensitive-info",
21 | "persist-storage"
22 | ],
23 | "author": {
24 | "name": "Meng-Tse Hang & VisuallyLab",
25 | "email": "a54383813@gmail.com"
26 | },
27 | "bugs": {
28 | "url": "https://github.com/visuallylab/react-native-usePersistStorage/issues"
29 | },
30 | "license": "MIT",
31 | "peerDependencies": {
32 | "react": ">=16.8.1",
33 | "react-native": ">=0.59.0"
34 | },
35 | "dependencies": {
36 | "@react-native-async-storage/async-storage": "^1.15.1",
37 | "react-native-sensitive-info": "^5.5.4"
38 | },
39 | "devDependencies": {
40 | "@babel/core": "^7.7.5",
41 | "@babel/preset-env": "^7.7.6",
42 | "@babel/preset-typescript": "^7.7.4",
43 | "@testing-library/react-hooks": "^3.2.1",
44 | "@types/jest": "^24.0.23",
45 | "@types/react": "^16.9.16",
46 | "@types/react-native": "^0.60.25",
47 | "@visuallylab/tslint-config-frontend": "^1.0.13",
48 | "babel-jest": "^24.9.0",
49 | "babel-plugin-transform-flow-strip-types": "^6.22.0",
50 | "jest": "^24.9.0",
51 | "react": "^16.9.0",
52 | "react-native": "^0.61.5",
53 | "react-test-renderer": "^16.9.0",
54 | "tslint": "^5.20.1",
55 | "typescript": "^3.7.3"
56 | },
57 | "jest": {
58 | "setupFiles": [
59 | "./tests/setup.ts"
60 | ],
61 | "transformIgnorePatterns": [
62 | "node_modules/(?!(@react-native-community|react-native)/)"
63 | ]
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/createAsyncStorage.ts:
--------------------------------------------------------------------------------
1 | import asyncStorage from '@react-native-async-storage/async-storage';
2 |
3 | type TValue = string | null;
4 |
5 | type TCallback = (err: any, data: TValue | any[]) => void;
6 |
7 | const noop: TCallback = () => null;
8 |
9 | const createAsyncStorage = () => {
10 | return {
11 | async getItem(key: string, callback = noop) {
12 | try {
13 | const result: TValue = await asyncStorage.getItem(key);
14 |
15 | callback(null, result);
16 |
17 | return result;
18 | } catch (error) {
19 | callback(error, null);
20 | throw error;
21 | }
22 | },
23 |
24 | async setItem(key: string, value: string, callback = noop) {
25 | try {
26 | await asyncStorage.setItem(key, value);
27 | callback(null, value);
28 | } catch (error) {
29 | callback(error, null);
30 | throw error;
31 | }
32 | },
33 |
34 | async removeItem(key: string, callback = noop) {
35 | try {
36 | await asyncStorage.removeItem(key);
37 | callback(null, null);
38 | } catch (error) {
39 | callback(error, null);
40 | throw error;
41 | }
42 | },
43 | }
44 | };
45 |
46 | export default createAsyncStorage;
47 |
--------------------------------------------------------------------------------
/src/createMigrate.ts:
--------------------------------------------------------------------------------
1 | import { defaultOptions } from "./defaultOptions";
2 | import { PersistStorageValue } from "./usePersistStorage";
3 | import { transformStorageValue } from "./utils";
4 |
5 | export type TMigrations = {
6 | [version: number]: (state: any) => any;
7 | };
8 |
9 | export type TMigrationFuncParams = {
10 | key: string;
11 | state: PersistStorageValue;
12 | version: number;
13 | };
14 |
15 | export type TCreateMigrateConfig = {
16 | debug?: boolean;
17 | };
18 |
19 | const createMigrate = (
20 | migrations: TMigrations,
21 | configs: TCreateMigrateConfig = { debug: defaultOptions.debug }
22 | ) => {
23 | return ({
24 | key,
25 | state,
26 | version
27 | }: TMigrationFuncParams): PersistStorageValue => {
28 | const { debug } = configs;
29 | if (!state.value) {
30 | if (debug) {
31 | console.debug(`[${key}]: no inbound value, skipping migration`);
32 | }
33 | return state;
34 | }
35 | if (state._currentVersion === version) {
36 | if (debug) {
37 | console.debug(`[${key}]: version match, no migration`);
38 | }
39 | return state;
40 | }
41 | if (state._currentVersion > version) {
42 | console.warn(`[${key}]: downgrading version is not supported`);
43 | return state;
44 | }
45 |
46 | const migrationKeys = Object.keys(migrations)
47 | .map(v => parseInt(v, 10))
48 | .filter(ver => version >= ver && ver > state._currentVersion)
49 | .sort((a, b) => a - b);
50 |
51 | if (debug) {
52 | console.debug(`[${key}]: migration keys`, migrationKeys);
53 | }
54 | const migrated = migrationKeys.reduce(({ value }, versionKey) => {
55 | if (debug) {
56 | console.warn(`[${key}]: running migration ${versionKey}`);
57 | }
58 | return transformStorageValue(migrations[versionKey](value), versionKey);
59 | }, state);
60 |
61 | return migrated;
62 | };
63 | };
64 |
65 | export default createMigrate;
66 |
--------------------------------------------------------------------------------
/src/createPersistContext.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import usePersistStorage, {
3 | UsePersistStorageOptions,
4 | AsyncSetState
5 | } from "./usePersistStorage";
6 |
7 | export type PersistContext = [
8 | T,
9 | AsyncSetState,
10 | boolean
11 | ];
12 |
13 | const createPersistContext = ({
14 | storageKey,
15 | defaultData,
16 | options
17 | }: {
18 | storageKey: string;
19 | defaultData: T;
20 | options?: UsePersistStorageOptions;
21 | }) => {
22 | const createDefaultData = () => defaultData;
23 |
24 | const Context = React.createContext>([
25 | createDefaultData(), // state
26 | async () => {;}, // update state function
27 | false // restored
28 | ]);
29 |
30 | const Provider: React.FC<{
31 | persist?: boolean;
32 | }> = props => {
33 | const [data, setData, restored] = usePersistStorage(
34 | storageKey,
35 | createDefaultData,
36 | {
37 | persist: props.persist,
38 | ...options
39 | }
40 | );
41 |
42 | return (
43 |
44 | {props.children}
45 |
46 | );
47 | };
48 |
49 | const useData = () => {
50 | const context = React.useContext(Context);
51 | if (!context) {
52 | throw new Error(
53 | `Context error: context [${storageKey}] must be used within a Provider`
54 | );
55 | }
56 | return context;
57 | };
58 |
59 | return {
60 | Provider,
61 | Context,
62 | useData
63 | };
64 | };
65 |
66 | export default createPersistContext;
67 |
--------------------------------------------------------------------------------
/src/createSensitiveStorage.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from 'react-native';
2 | import sensitiveInfo, {
3 | RNSensitiveInfoOptions,
4 | } from 'react-native-sensitive-info';
5 |
6 | type TValue = string | null;
7 |
8 | type TCallback = (err: any, data: TValue | any[]) => void;
9 |
10 | // react-native-sensitive-info returns different a different structure on iOS
11 | // than it does on Android.
12 | //
13 | // iOS:
14 | // [
15 | // [
16 | // { service: 'app', key: 'foo', value: 'bar' },
17 | // { service: 'app', key: 'baz', value: 'quux' }
18 | // ]
19 | // ]
20 | //
21 | // Android:
22 | // {
23 | // foo: 'bar',
24 | // baz: 'quux'
25 | // }
26 | //
27 | // See https://github.com/mCodex/react-native-sensitive-info/issues/8
28 | //
29 | // `extractKeys` adapts for the different structure to return the list of keys.
30 | const extractKeys = Platform.select({
31 | ios: (items: [any[]]) => items[0].map(item => item.key),
32 | android: Object.keys,
33 | });
34 |
35 | const noop: TCallback = () => null;
36 |
37 | /**
38 | * Please use carefully, and store only secure information.
39 | * If you want to open TouchID or FaceID, please refer https://github.com/mCodex/react-native-sensitive-info#methods
40 | * @param options react-native-info options
41 | */
42 | const createSensitiveStorage = (sensitiveOpts: RNSensitiveInfoOptions = {}) => {
43 | return {
44 | async getItem(key: string, callback = noop) {
45 | try {
46 | // getItem() returns `null` on Android and `undefined` on iOS;
47 | // explicitly return `null` here as `undefined` causes an exception
48 | // upstream.
49 | let result: TValue = await sensitiveInfo.getItem(key, sensitiveOpts);
50 |
51 | if (typeof result === 'undefined') {
52 | result = null;
53 | }
54 |
55 | callback(null, result);
56 |
57 | return result;
58 | } catch (error) {
59 | callback(error, null);
60 | throw error;
61 | }
62 | },
63 |
64 | async setItem(key: string, value: string, callback = noop) {
65 | try {
66 | await sensitiveInfo.setItem(key, value, sensitiveOpts);
67 | callback(null, value);
68 | } catch (error) {
69 | callback(error, null);
70 | throw error;
71 | }
72 | },
73 |
74 | async removeItem(key: string, callback = noop) {
75 | try {
76 | await sensitiveInfo.deleteItem(key, sensitiveOpts);
77 | callback(null, null);
78 | } catch (error) {
79 | callback(error, null);
80 | throw error;
81 | }
82 | },
83 |
84 | async getAllKeys(callback = noop) {
85 | try {
86 | const values = (await sensitiveInfo.getAllItems(sensitiveOpts)) as any;
87 | const result = extractKeys(values);
88 |
89 | callback(null, result);
90 |
91 | return result;
92 | } catch (error) {
93 | callback(error, null);
94 | throw error;
95 | }
96 | },
97 | };
98 | };
99 |
100 | export default createSensitiveStorage;
101 |
--------------------------------------------------------------------------------
/src/defaultOptions.ts:
--------------------------------------------------------------------------------
1 |
2 | import { UsePersistStorageOptions } from './usePersistStorage';
3 |
4 | export let defaultOptions: UsePersistStorageOptions = {
5 | debug: false,
6 | version: 0,
7 | persist: true,
8 | migrate: null,
9 |
10 | // refer from https://github.com/mCodex/react-native-sensitive-info
11 | sensitive: false,
12 | };
13 |
14 | export type SetPersistStorageDefaultsParams = {
15 | debug?: boolean;
16 | persist?: boolean;
17 | version?: number;
18 | }
19 |
20 | export const setPersistStorageDefaults = (configs: SetPersistStorageDefaultsParams) => {
21 | defaultOptions = { ...defaultOptions, ...configs };
22 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./usePersistStorage";
2 | export { default as usePersistStorage } from "./usePersistStorage";
3 |
4 | export * from "./createPersistContext";
5 | export { default as createPersistContext } from "./createPersistContext";
6 |
7 | export * from "./createMigrate";
8 | export { default as createMigrate } from "./createMigrate";
9 |
10 | export { setPersistStorageDefaults } from "./defaultOptions";
11 |
--------------------------------------------------------------------------------
/src/usePersistStorage.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef, useMemo, useCallback } from "react";
2 | import { RNSensitiveInfoOptions } from "react-native-sensitive-info";
3 |
4 | import { TMigrationFuncParams } from "./createMigrate";
5 | import createAsyncStorage from "./createAsyncStorage";
6 | import createSensitiveStorage from "./createSensitiveStorage";
7 | import { defaultOptions } from "./defaultOptions";
8 | import { transformStorageValue } from "./utils";
9 |
10 | export interface PersistStorageValue {
11 | _currentVersion: number;
12 | value: Value;
13 | }
14 |
15 | export type UsePersistStorageOptions = {
16 | debug?: boolean;
17 | version?: number;
18 | persist?: boolean;
19 | migrate?:
20 | | ((params: TMigrationFuncParams) => PersistStorageValue)
21 | | null;
22 | sensitive?: false | RNSensitiveInfoOptions;
23 | };
24 |
25 | type CallbackFn = (prev: S) => S;
26 | export type AsyncSetState = (
27 | stateOrCallbackFn: S | CallbackFn
28 | ) => Promise;
29 |
30 | /**
31 | * usePersistStorage will return state that'll be consistent with your storage.
32 | * support migration and storing sensitive info
33 | * @param key the key stored in localStorage
34 | * @param initialValue defaultValue
35 | * @param options
36 | */
37 | const usePersistStorage = (
38 | key: string,
39 | initialValue: Value | (() => Value),
40 | {
41 | debug = defaultOptions.debug,
42 | version = defaultOptions.version,
43 | persist = defaultOptions.persist,
44 | migrate = defaultOptions.migrate,
45 | sensitive = defaultOptions.sensitive
46 | }: UsePersistStorageOptions = defaultOptions
47 | ): [Value, AsyncSetState, boolean] => {
48 | const currentVersion = useRef(version || 0);
49 | const [state, setState] = useState(initialValue);
50 | const [restored, setRestored] = useState(false);
51 |
52 | const Storage = useMemo(
53 | () =>
54 | sensitive ? createSensitiveStorage(sensitive) : createAsyncStorage(),
55 | [sensitive]
56 | );
57 |
58 | const logPrefix = sensitive ? "(sensitive)" : "";
59 |
60 | const setValueToStorage = useCallback(
61 | async (newValue: Value) => {
62 | const value = transformStorageValue(
63 | newValue,
64 | currentVersion.current
65 | );
66 | try {
67 | const serializedValue = JSON.stringify(value);
68 | await Storage.setItem(key, serializedValue);
69 | if (debug) {
70 | console.debug(`${logPrefix}[PersistStorage]: set ${key}: `, value);
71 | }
72 | } catch (err) {
73 | console.error(err);
74 | }
75 | },
76 | [Storage]
77 | );
78 |
79 | useEffect(() => {
80 | if (persist) {
81 | // Restore from storage when first mount.
82 | const restoreStateFromStorage = async () => {
83 | try {
84 | const storageValue = await Storage.getItem(key);
85 | if (storageValue) {
86 | let parsedValue = JSON.parse(storageValue || "null");
87 |
88 | // format if value is incorrect
89 | if (parsedValue && parsedValue._currentVersion === undefined) {
90 | parsedValue = transformStorageValue(
91 | parsedValue,
92 | currentVersion.current
93 | );
94 | }
95 |
96 | if (migrate) {
97 | parsedValue = migrate({
98 | key,
99 | state: parsedValue,
100 | version: currentVersion.current
101 | });
102 | currentVersion.current = parsedValue._currentVersion;
103 | await setValueToStorage(parsedValue.value);
104 | }
105 |
106 | setState(parsedValue.value);
107 | if (debug) {
108 | console.debug(
109 | `${logPrefix}[PersistStorage]: restore ${key}: `,
110 | parsedValue
111 | );
112 | }
113 | } else {
114 | // If storage has no value, set initial value to storage
115 | await setValueToStorage(state);
116 | }
117 | } catch (err) {
118 | console.error(err);
119 | }
120 |
121 | setRestored(true);
122 | };
123 |
124 | restoreStateFromStorage();
125 | } else {
126 | // If disable persist, remove storageValue.
127 | const removePersistItem = async () => {
128 | await Storage.removeItem(key);
129 | setRestored(true);
130 | };
131 |
132 | removePersistItem();
133 | if (debug) {
134 | console.debug(`${logPrefix}[PersistStorage]: remove ${key}`);
135 | }
136 | }
137 | }, []);
138 |
139 | const asyncSetState: AsyncSetState = async stateOrCallbackFn => {
140 | const newValue: Value =
141 | stateOrCallbackFn instanceof Function
142 | ? stateOrCallbackFn(state)
143 | : stateOrCallbackFn;
144 |
145 | setState(newValue);
146 | if (persist) {
147 | await setValueToStorage(newValue);
148 | }
149 | };
150 |
151 | return [state, asyncSetState, restored];
152 | };
153 |
154 | export default usePersistStorage;
155 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { PersistStorageValue } from "./usePersistStorage";
2 |
3 | export function transformStorageValue(
4 | value: T,
5 | version = 0
6 | ): PersistStorageValue {
7 | return {
8 | _currentVersion: version,
9 | value
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/tests/__mocks__/@react-native-community/async-storage.js:
--------------------------------------------------------------------------------
1 | import mockStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock';
2 | export default mockStorage;
3 |
--------------------------------------------------------------------------------
/tests/__mocks__/react-native-sensitive-info.js:
--------------------------------------------------------------------------------
1 | export default {
2 | setItem: jest.fn(),
3 | getItem: jest.fn(),
4 | deleteItem: jest.fn(),
5 | getAllItems: jest.fn(),
6 | };
7 |
--------------------------------------------------------------------------------
/tests/createAsyncStorage.test.ts:
--------------------------------------------------------------------------------
1 | import createAsyncStorage from "../src/createAsyncStorage";
2 |
3 | const KEY = "@Test";
4 | const { getItem, setItem, removeItem } = createAsyncStorage();
5 |
6 | beforeAll(async () => {
7 | await removeItem(KEY);
8 | })
9 |
10 | afterAll(async () => {
11 | await removeItem(KEY);
12 | })
13 |
14 | test("test init getItem", async () => {
15 | let err;
16 | let data;
17 | const val = await getItem(KEY, (e, d) => {
18 | err = e;
19 | data = d;
20 | });
21 | expect(err).toBeNull();
22 | expect(data).toBeNull();
23 | expect(val).toBeNull();
24 | });
25 |
26 | test("test setItem", async () => {
27 | let err;
28 | let data;
29 | await setItem(KEY, "test", (e, d) => {
30 | err = e;
31 | data = d;
32 | });
33 | expect(err).toBeNull();
34 | expect(data).toBe('test');
35 |
36 | const val = await getItem(KEY);
37 | expect(val).toBe("test");
38 | });
39 |
40 | test("test removeItem", async () => {
41 | await removeItem(KEY);
42 | const val = await getItem(KEY);
43 | expect(val).toBeNull();
44 | });
45 |
--------------------------------------------------------------------------------
/tests/createMigrate.test.ts:
--------------------------------------------------------------------------------
1 | import { renderHook, act } from "@testing-library/react-hooks";
2 | import usePersistStorage from "../src/usePersistStorage";
3 | import createAsyncStorage from "../src/createAsyncStorage";
4 | import createMigrate from "../src/createMigrate";
5 | import { sleep } from "./utils";
6 |
7 | //
8 | // {
9 | // _currentVersion: number;
10 | // value: Value;
11 | // };
12 | const transformStorageValue = (value: any, version = 0) =>
13 | JSON.stringify({
14 | _currentVersion: version,
15 | value
16 | });
17 |
18 | const KEY = "@TEST";
19 | const store = createAsyncStorage();
20 |
21 | beforeAll(async () => {
22 | await store.removeItem(KEY);
23 | });
24 |
25 | afterAll(async () => {
26 | await store.removeItem(KEY);
27 | });
28 |
29 | test("version 0", async () => {
30 | const { result } = renderHook(
31 | props => usePersistStorage(KEY, "test", props),
32 | {
33 | initialProps: { version: 0, migrate: undefined }
34 | }
35 | );
36 |
37 | expect(result.current[0]).toBe("test");
38 | expect(typeof result.current[1]).toBe("function");
39 |
40 | // wait update asyncStorage;
41 | await sleep(100);
42 | expect(await store.getItem(KEY)).toBe(transformStorageValue("test", 0));
43 | });
44 |
45 | test("restore & migrate version 1", async () => {
46 | const { result } = renderHook(
47 | props => usePersistStorage(KEY, "test", props),
48 | {
49 | initialProps: {
50 | version: 1,
51 | migrate: createMigrate({
52 | 1: state => state + " migrate!"
53 | })
54 | }
55 | }
56 | );
57 |
58 | // wait restore and migrate asyncStorage;
59 | await sleep(300);
60 | expect(result.current[0]).toBe("test migrate!");
61 | expect(typeof result.current[1]).toBe("function");
62 | expect(await store.getItem(KEY)).toBe(
63 | transformStorageValue("test migrate!", 1)
64 | );
65 | });
66 |
--------------------------------------------------------------------------------
/tests/setup.ts:
--------------------------------------------------------------------------------
1 | jest.mock('react-native', () => ({
2 | Platform: {
3 | select: jest.fn(),
4 | }
5 | }));
--------------------------------------------------------------------------------
/tests/usePersistStorage.test.ts:
--------------------------------------------------------------------------------
1 | import { renderHook, act } from "@testing-library/react-hooks";
2 | import usePersistStorage from "../src/usePersistStorage";
3 | import createAsyncStorage from "../src/createAsyncStorage";
4 | import { transformStorageValue } from "../src/utils";
5 | import { sleep } from "./utils";
6 |
7 | const toStorageValue = (value: any, version = 0) =>
8 | JSON.stringify(transformStorageValue(value, version));
9 |
10 | const KEY = "@TEST";
11 | const store = createAsyncStorage();
12 |
13 | beforeAll(async () => {
14 | await store.removeItem(KEY);
15 | });
16 |
17 | afterAll(async () => {
18 | await store.removeItem(KEY);
19 | });
20 |
21 | test("init, setState, version = 10", async () => {
22 | const { result } = renderHook(() =>
23 | usePersistStorage(KEY, "test", { version: 10 })
24 | );
25 |
26 | expect(result.current[0]).toBe("test");
27 | expect(typeof result.current[1]).toBe("function");
28 | expect(result.current[2]).toBe(false);
29 |
30 | await sleep(100); // wait mount && init asyncStorage;
31 | expect(await store.getItem(KEY)).toBe(toStorageValue("test", 10));
32 | expect(result.current[2]).toBe(true);
33 |
34 | act(() => {
35 | result.current[1]("change");
36 | });
37 | expect(result.current[0]).toBe("change");
38 |
39 | await sleep(100); // wait update asyncStorage;
40 | expect(await store.getItem(KEY)).toBe(toStorageValue("change", 10));
41 |
42 | act(async () => {
43 | await result.current[1]("async change"); // async setState
44 | });
45 | expect(result.current[0]).toBe("async change");
46 | expect(await store.getItem(KEY)).toBe(toStorageValue("async change", 10));
47 |
48 | act(async () => {
49 | await result.current[1](prev => prev + " callback"); // async setState callback
50 | });
51 | expect(result.current[0]).toBe("async change callback");
52 | expect(await store.getItem(KEY)).toBe(toStorageValue("async change callback", 10));
53 |
54 | });
55 |
56 | test("restore state", async () => {
57 | const { result } = renderHook(() => usePersistStorage(KEY, "test"));
58 |
59 | await sleep(300); // wait restore
60 | expect(result.current[0]).toBe("async change callback");
61 | expect(typeof result.current[1]).toBe("function");
62 | expect(result.current[2]).toBe(true);
63 | expect(await store.getItem(KEY)).toBe(toStorageValue("async change callback", 10));
64 | });
65 |
66 | test("no persist state", async () => {
67 | const { result } = renderHook(() => usePersistStorage(KEY, "test", { persist: false }));
68 |
69 | await sleep(300); // wait remove item
70 | expect(result.current[0]).toBe("test");
71 | expect(typeof result.current[1]).toBe("function");
72 | expect(await store.getItem(KEY)).toBeNull();
73 |
74 | act(() => {
75 | result.current[1]("change");
76 | });
77 |
78 | expect(result.current[0]).toBe("change");
79 | expect(await store.getItem(KEY)).toBeNull();
80 | });
81 |
--------------------------------------------------------------------------------
/tests/utils.ts:
--------------------------------------------------------------------------------
1 | export const sleep = (ms: number) =>
2 | new Promise(resolve => setTimeout(() => resolve(), ms));
3 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist",
4 | "declarationDir": "types",
5 | "module": "commonjs",
6 | "noImplicitAny": true,
7 | "removeComments": true,
8 | "preserveConstEnums": true,
9 | "strictNullChecks": true,
10 | "moduleResolution": "node",
11 | "esModuleInterop": true,
12 | "experimentalDecorators": true,
13 | "jsx": "react",
14 | "noUnusedParameters": true,
15 | "noUnusedLocals": true,
16 | "target": "es5",
17 | "lib": ["es6", "es2017"],
18 | "declaration": true
19 | },
20 | "include": ["src/**/*"],
21 | "exclude": ["node_modules"]
22 | }
23 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@visuallylab/tslint-config-frontend"],
3 | "rules": {
4 | "no-console": [true, "log"],
5 | "interface-name": false
6 | }
7 | }
--------------------------------------------------------------------------------
/types/createAsyncStorage.d.ts:
--------------------------------------------------------------------------------
1 | declare type TValue = string | null;
2 | declare type TCallback = (err: any, data: TValue | any[]) => void;
3 | declare const createAsyncStorage: () => {
4 | getItem(key: string, callback?: TCallback): Promise;
5 | setItem(key: string, value: string, callback?: TCallback): Promise;
6 | removeItem(key: string, callback?: TCallback): Promise;
7 | };
8 | export default createAsyncStorage;
9 |
--------------------------------------------------------------------------------
/types/createMigrate.d.ts:
--------------------------------------------------------------------------------
1 | import { PersistStorageValue } from "./usePersistStorage";
2 | export declare type TMigrations = {
3 | [version: number]: (state: any) => any;
4 | };
5 | export declare type TMigrationFuncParams = {
6 | key: string;
7 | state: PersistStorageValue;
8 | version: number;
9 | };
10 | export declare type TCreateMigrateConfig = {
11 | debug?: boolean;
12 | };
13 | declare const createMigrate: (migrations: TMigrations, configs?: TCreateMigrateConfig) => ({ key, state, version }: TMigrationFuncParams) => PersistStorageValue;
14 | export default createMigrate;
15 |
--------------------------------------------------------------------------------
/types/createPersistContext.d.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { UsePersistStorageOptions, AsyncSetState } from "./usePersistStorage";
3 | export declare type PersistContext = [T, AsyncSetState, boolean];
4 | declare const createPersistContext: ({ storageKey, defaultData, options }: {
5 | storageKey: string;
6 | defaultData: T;
7 | options?: UsePersistStorageOptions | undefined;
8 | }) => {
9 | Provider: React.FC<{
10 | persist?: boolean | undefined;
11 | }>;
12 | Context: React.Context>;
13 | useData: () => PersistContext;
14 | };
15 | export default createPersistContext;
16 |
--------------------------------------------------------------------------------
/types/createSensitiveStorage.d.ts:
--------------------------------------------------------------------------------
1 | import sensitiveInfo from 'react-native-sensitive-info';
2 | declare type TValue = string | null;
3 | declare type TCallback = (err: any, data: TValue | any[]) => void;
4 | declare const createSensitiveStorage: (sensitiveOpts?: sensitiveInfo.RNSensitiveInfoOptions) => {
5 | getItem(key: string, callback?: TCallback): Promise;
6 | setItem(key: string, value: string, callback?: TCallback): Promise;
7 | removeItem(key: string, callback?: TCallback): Promise;
8 | getAllKeys(callback?: TCallback): Promise;
9 | };
10 | export default createSensitiveStorage;
11 |
--------------------------------------------------------------------------------
/types/defaultOptions.d.ts:
--------------------------------------------------------------------------------
1 | import { UsePersistStorageOptions } from './usePersistStorage';
2 | export declare let defaultOptions: UsePersistStorageOptions;
3 | export declare type SetPersistStorageDefaultsParams = {
4 | debug?: boolean;
5 | persist?: boolean;
6 | version?: number;
7 | };
8 | export declare const setPersistStorageDefaults: (configs: SetPersistStorageDefaultsParams) => void;
9 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from "./usePersistStorage";
2 | export { default as usePersistStorage } from "./usePersistStorage";
3 | export * from "./createPersistContext";
4 | export { default as createPersistContext } from "./createPersistContext";
5 | export * from "./createMigrate";
6 | export { default as createMigrate } from "./createMigrate";
7 | export { setPersistStorageDefaults } from "./defaultOptions";
8 |
--------------------------------------------------------------------------------
/types/usePersistStorage.d.ts:
--------------------------------------------------------------------------------
1 | import { RNSensitiveInfoOptions } from "react-native-sensitive-info";
2 | import { TMigrationFuncParams } from "./createMigrate";
3 | export interface PersistStorageValue {
4 | _currentVersion: number;
5 | value: Value;
6 | }
7 | export declare type UsePersistStorageOptions = {
8 | debug?: boolean;
9 | version?: number;
10 | persist?: boolean;
11 | migrate?: ((params: TMigrationFuncParams) => PersistStorageValue) | null;
12 | sensitive?: false | RNSensitiveInfoOptions;
13 | };
14 | declare type CallbackFn = (prev: S) => S;
15 | export declare type AsyncSetState = (stateOrCallbackFn: S | CallbackFn) => Promise;
16 | declare const usePersistStorage: (key: string, initialValue: Value | (() => Value), { debug, version, persist, migrate, sensitive }?: UsePersistStorageOptions) => [Value, AsyncSetState, boolean];
17 | export default usePersistStorage;
18 |
--------------------------------------------------------------------------------
/types/utils.d.ts:
--------------------------------------------------------------------------------
1 | import { PersistStorageValue } from "./usePersistStorage";
2 | export declare function transformStorageValue(value: T, version?: number): PersistStorageValue;
3 |
--------------------------------------------------------------------------------