Skip to content

Commit 170bd1b

Browse files
committed
Handle Android secure store corruption.
1 parent 5a0798a commit 170bd1b

File tree

5 files changed

+164
-34
lines changed

5 files changed

+164
-34
lines changed

jest.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,11 @@ jest.mock('expo-secure-store', () => {
188188
async getItemAsync (key: string, options?: unknown): Promise<null | string> {
189189
if (options === undefined) {
190190
await new Promise<void>((resolve) => setTimeout(resolve, 50))
191-
return encryptedStorage.get(key) ?? null
191+
if (key === 'Test Error-Throwing Key') {
192+
throw new Error('Test Error')
193+
} else {
194+
return encryptedStorage.get(key) ?? null
195+
}
192196
} else {
193197
throw new Error(
194198
'expo-secure-store.getItemAsync\'s mock does not support options.'

react-native/components/createSessionStoreManagerComponent/unit.tsx

+6-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { createSessionStoreManagerComponent, SessionStore } from '../../..'
88
type TestSession = { readonly value: number }
99

1010
test('displays the loading screen', async () => {
11-
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase())
11+
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase(), { report: jest.fn() })
1212
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore)
1313

1414
const renderer = TestRenderer.create(
@@ -45,7 +45,7 @@ test('displays the loading screen', async () => {
4545
})
4646

4747
test('shows the ready screen once given time to load', async () => {
48-
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase())
48+
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase(), { report: jest.fn() })
4949
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore)
5050

5151
const renderer = TestRenderer.create(
@@ -84,7 +84,7 @@ test('shows the ready screen once given time to load', async () => {
8484
})
8585

8686
test('re-renders when the session is changed externally once', async () => {
87-
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase())
87+
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase(), { report: jest.fn() })
8888
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore)
8989

9090
const renderer = TestRenderer.create(
@@ -124,7 +124,7 @@ test('re-renders when the session is changed externally once', async () => {
124124
})
125125

126126
test('re-renders when the session is changed externally twice', async () => {
127-
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase())
127+
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase(), { report: jest.fn() })
128128
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore)
129129

130130
const renderer = TestRenderer.create(
@@ -165,7 +165,7 @@ test('re-renders when the session is changed externally twice', async () => {
165165
})
166166

167167
test('re-renders when the session is changed internally once', async () => {
168-
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase())
168+
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase(), { report: jest.fn() })
169169
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore)
170170

171171
const renderer = TestRenderer.create(
@@ -207,7 +207,7 @@ test('re-renders when the session is changed internally once', async () => {
207207
})
208208

209209
test('re-renders when the session is changed internally twice', async () => {
210-
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase())
210+
const sessionStore = new SessionStore<TestSession>({ value: 5 }, randomUUID().toLowerCase(), { report: jest.fn() })
211211
const SessionStoreManager = createSessionStoreManagerComponent(sessionStore)
212212

213213
const renderer = TestRenderer.create(

react-native/services/SessionStore/index.tsx

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as SecureStore from 'expo-secure-store'
22
import { EventEmitter } from 'events'
33
import type { Json } from '../../types/Json'
4+
import type { ErrorReporterInterface } from '../../types/ErrorReporterInterface'
45

56
/**
67
* A wrapper around expo-secure-store which adds:
@@ -23,10 +24,12 @@ export class SessionStore<T extends Json> {
2324
* store.
2425
* @param secureStorageKey The key of the record to read from/write to
2526
* expo-secure-store.
27+
* @param errorReporter The error reporter to use.
2628
*/
2729
constructor (
2830
private readonly initial: T,
29-
private readonly secureStorageKey: string
31+
private readonly secureStorageKey: string,
32+
private readonly errorReporter: ErrorReporterInterface
3033
) {}
3134

3235
/**
@@ -64,7 +67,13 @@ export class SessionStore<T extends Json> {
6467
} else {
6568
this.unloaded = false
6669

67-
const raw = await SecureStore.getItemAsync(this.secureStorageKey)
70+
let raw: null | string = null
71+
72+
try {
73+
raw = await SecureStore.getItemAsync(this.secureStorageKey)
74+
} catch (e) {
75+
this.errorReporter.report(e)
76+
}
6877

6978
if (raw === null) {
7079
this.value = this.initial

react-native/services/SessionStore/readme.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,24 @@ A wrapper around `expo-secure-store` which adds:
77
- Change events.
88
- A synchronous read/write API (with asynchronous write-back).
99

10+
## Android decryption failure handling
11+
12+
Expo on Android unfortunately has a tendency to lose the ability to decrypt the
13+
secure store. It's not known why this is, but when it happens, the only
14+
workaround is to catch the exception thrown by `expo-secure-store` and continue
15+
as though the store is empty.
16+
17+
For this reason, any exceptions thrown by `expo-secure-store` during the load
18+
phase are ignored.
19+
1020
## Usage
1121

1222
```tsx
13-
import type { SessionStore } from "react-native-app-helpers";
23+
import type { SessionStore, errorReporter } from "react-native-app-helpers";
1424

1525
type Session = `Session A` | `Session B`;
1626

17-
const store = new SessionStore<Session>(`Session A`, `SecureStorage Key`);
27+
const store = new SessionStore<Session>(`Session A`, `SecureStorage Key`, errorReporter);
1828

1929

2030
await store.load();

0 commit comments

Comments
 (0)