diff --git a/README.md b/README.md
index 8e1207f31..0bd6015c1 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ yarn add @segment/analytics-react-native @segment/sovran-react-native react-nati
npm install --save @segment/analytics-react-native @segment/sovran-react-native react-native-get-random-values @react-native-async-storage/async-storage
```
-_Note: `@react-native-async-storage/async-storage` is an optional dependency. If you wish to use your own persistence layer you can use the `storePersistor` option when initializing the client. Make sure you always have a persistor (either by having AsyncStorage package installed or by explicitly passing a value), else you might get unexpected side-effects like multiple 'Application Installed' events. Read more [Client Options](#client-options)_
+_Note: `@react-native-async-storage/async-storage` is an optional dependency. If you wish to use your own persistence layer you can use the `storePersistor` option when initializing the client. Make sure you always have a persistor (either by having AsyncStorage package installed or by explicitly passing a value), else you might get unexpected side-effects like multiple 'Application Installed' events. Read more [Client Options](#client-options). ⚠️ AsyncStorage is unencrypted—see [Data Storage & Security](#data-storage--security) before storing sensitive PII._
For iOS, install native modules with:
@@ -98,7 +98,7 @@ You must pass at least the `writeKey`. Additional configuration options are list
| `trackDeepLinks` | false | Enable automatic tracking for when the user opens the app via a deep link (Note: Requires additional setup on iOS, [see instructions](#ios-deep-link-tracking-setup)). |
| `defaultSettings` | undefined | Settings that will be used if the request to get the settings from Segment fails. Type: [SegmentAPISettings](https://github.com/segmentio/analytics-react-native/blob/c0a5895c0c57375f18dd20e492b7d984393b7bc4/packages/core/src/types.ts#L293-L299) |
| `autoAddSegmentDestination` | true | Set to false to skip adding the SegmentDestination plugin. |
-| `storePersistor` | undefined | A custom persistor for the store that `analytics-react-native` leverages. Must match [`Persistor`](https://github.com/segmentio/analytics-react-native/blob/master/packages/sovran/src/persistor/persistor.ts#L1-L18) interface exported from [sovran-react-native](https://github.com/segmentio/analytics-react-native/blob/master/packages/sovran). |
+| `storePersistor` | undefined | A custom persistor for the store that `analytics-react-native` leverages. Must match [`Persistor`](https://github.com/segmentio/analytics-react-native/blob/master/packages/sovran/src/persistor/persistor.ts#L20-L31) interface exported from [sovran-react-native](https://github.com/segmentio/analytics-react-native/blob/master/packages/sovran). ⚠️ The default (AsyncStorage) persistor is unencrypted, see [Data Storage & Security](#data-storage--security). |
| `proxy` | undefined | `proxy` is a batch URL to post the events. Enable `useSegmentEndpoint` if proxy domain is provided and you want to append the Segment endpoints automatically. If you want to completely customize the proxy by providing a custom URL, disable `useSegmentEndpoint`. Default value is `false`. |
| `errorHandler` | undefined | Create custom actions when errors happen, see [Handling errors](#handling-errors). |
| `cdnProxy` | undefined | Sets an alternative CDN host for settings retrieval. Enable `useSegmentEndpoint` if `cdnProxy` domain is provided and you want to append the Segment endpoints automatically.
⚠️ **IMPORTANT NOTE:** _Prior to version 2.20.4, any value provided for `cdnProxy` was automatically appended with `/write-key/settings`. **Starting from v2.20.4**, this behavior has **CHANGED**—the SDK will now behave based on the `useSegmentEndpoint` flag._ Please update your configuration accordingly to avoid unexpected issues. ⚠️ |
@@ -106,6 +106,47 @@ You must pass at least the `writeKey`. Additional configuration options are list
\* The default value of `debug` will be false in production.
+### Data Storage & Security
+
+By default, `analytics-react-native` persists its state—`userId`, `identify` traits, and the queue of events pending upload—to disk using [`@react-native-async-storage/async-storage`](https://github.com/react-native-async-storage/async-storage). **This default persistor stores data as plaintext JSON with no encryption.** On a rooted/jailbroken device, via an ADB backup, or through forensic extraction, that data—including any PII passed to `identify` or `track` calls—can be read.
+
+If your app handles sensitive PII (email, name, government IDs, etc.), supply your own `storePersistor` backed by encrypted storage, such as the OS Keychain/Keystore via [`react-native-encrypted-storage`](https://github.com/emeraldsanto/react-native-encrypted-storage) or [`react-native-keychain`](https://github.com/oblador/react-native-keychain). Your persistor must implement the [`Persistor`](https://github.com/segmentio/analytics-react-native/blob/master/packages/sovran/src/persistor/persistor.ts#L20-L31) interface:
+
+```ts
+import EncryptedStorage from 'react-native-encrypted-storage';
+import type { Persistor } from '@segment/sovran-react-native';
+import { createClient } from '@segment/analytics-react-native';
+
+const EncryptedStoragePersistor: Persistor = {
+ get: async (key) => {
+ try {
+ const value = await EncryptedStorage.getItem(key);
+ return value ? JSON.parse(value) : undefined;
+ } catch {
+ return undefined;
+ }
+ },
+ set: async (key, state) => {
+ try {
+ await EncryptedStorage.setItem(key, JSON.stringify(state));
+ } catch (e) {
+ console.warn('Failed to persist Segment state', e);
+ }
+ },
+};
+
+const segmentClient = createClient({
+ writeKey: 'SEGMENT_API_KEY',
+ storePersistor: EncryptedStoragePersistor,
+ // Batch writes: the default of 0 re-encrypts the whole queue on every event.
+ storePersistorSaveDelay: 1000,
+});
+```
+
+_Note: Both methods catch errors on purpose. If `get` rejects (for example a corrupted blob, or the Keychain being inaccessible when the app launches in the background before first device unlock), the SDK never finishes restoring its state and stops sending events. If `set` rejects, state silently stops persisting across restarts. The pending-events queue is stored as a single blob and can grow large, which is the most likely cause of a failed write._
+
+⚠️ **Migrating existing apps:** Each store is read only through the configured persistor, so switching an app that already shipped with the default AsyncStorage persistor does not carry over its saved state. Existing users would get a new `anonymousId`, a spurious "Application Installed" event, and lose any events queued at the time of the upgrade. If this matters for your app, read the existing AsyncStorage values (keyed by store ID) once and seed the encrypted store before switching over, then remove the plaintext copies.
+
### iOS Deep Link Tracking Setup
_Note: This is only required for iOS if you are using the `trackDeepLinks` option. Android does not require any additional setup_
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 4cbd6b5ba..c440a9811 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -148,6 +148,13 @@ export type Config = {
defaultSettings?: SegmentAPISettings;
autoAddSegmentDestination?: boolean;
collectDeviceId?: boolean;
+ /**
+ * Custom persistor for the store. When omitted, the SDK defaults to
+ * AsyncStorage, which persists state (userId, `identify` traits, and the
+ * pending event queue) as plaintext JSON with no encryption. Apps handling
+ * sensitive PII should supply a `storePersistor` backed by encrypted
+ * storage (e.g. Keychain/Keystore via `react-native-encrypted-storage`).
+ */
storePersistor?: Persistor;
storePersistorSaveDelay?: number;
proxy?: string;
diff --git a/packages/sovran/src/persistor/async-storage-persistor.ts b/packages/sovran/src/persistor/async-storage-persistor.ts
index 0ec8b7934..63db995b9 100644
--- a/packages/sovran/src/persistor/async-storage-persistor.ts
+++ b/packages/sovran/src/persistor/async-storage-persistor.ts
@@ -31,7 +31,14 @@ function warnIfMissingPackage() {
}
/**
- * Persistor implementation using AsyncStorage
+ * Persistor implementation using AsyncStorage.
+ *
+ * ⚠️ AsyncStorage is unencrypted: state is written as plaintext JSON and is
+ * readable on rooted/jailbroken devices, via ADB backup, or forensic
+ * extraction. This includes userId, `identify` traits (email/name/etc.), and
+ * the pending event queue. Apps handling sensitive PII should pass their own
+ * `storePersistor` backed by encrypted storage (Keychain/Keystore, e.g. via
+ * `react-native-encrypted-storage`) instead of relying on this default.
*/
export const AsyncStoragePersistor: Persistor = {
get: async (key: string): Promise => {