feat(sdk): add offline mode to the react-native SDK
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import type { OpenPanelOptions, TrackProperties } from '@openpanel/sdk';
|
||||
import type {
|
||||
OpenPanelOptions,
|
||||
TrackHandlerPayload,
|
||||
TrackProperties,
|
||||
} from '@openpanel/sdk';
|
||||
import { OpenPanel as OpenPanelBase } from '@openpanel/sdk';
|
||||
import * as Application from 'expo-application';
|
||||
import Constants from 'expo-constants';
|
||||
@@ -6,9 +10,49 @@ import { AppState, Platform } from 'react-native';
|
||||
|
||||
export * from '@openpanel/sdk';
|
||||
|
||||
const QUEUE_STORAGE_KEY = '@openpanel/offline_queue';
|
||||
|
||||
interface StorageLike {
|
||||
getItem(key: string): Promise<string | null>;
|
||||
setItem(key: string, value: string): Promise<void>;
|
||||
}
|
||||
|
||||
interface NetworkStateLike {
|
||||
isConnected: boolean | null;
|
||||
}
|
||||
|
||||
interface NetworkInfoLike {
|
||||
addEventListener(callback: (state: NetworkStateLike) => void): () => void;
|
||||
fetch(): Promise<NetworkStateLike>;
|
||||
}
|
||||
|
||||
export interface ReactNativeOpenPanelOptions extends OpenPanelOptions {
|
||||
/**
|
||||
* Provide an AsyncStorage-compatible adapter to persist the event queue
|
||||
* across app restarts (enables full offline support).
|
||||
*
|
||||
* @example
|
||||
* import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
* new OpenPanel({ clientId: '...', storage: AsyncStorage });
|
||||
*/
|
||||
storage?: StorageLike;
|
||||
/**
|
||||
* Provide a NetInfo-compatible adapter to detect connectivity changes and
|
||||
* automatically flush the queue when the device comes back online.
|
||||
*
|
||||
* @example
|
||||
* import NetInfo from '@react-native-community/netinfo';
|
||||
* new OpenPanel({ clientId: '...', networkInfo: NetInfo });
|
||||
*/
|
||||
networkInfo?: NetworkInfoLike;
|
||||
}
|
||||
|
||||
export class OpenPanel extends OpenPanelBase {
|
||||
private lastPath = '';
|
||||
constructor(public options: OpenPanelOptions) {
|
||||
private readonly storage?: StorageLike;
|
||||
private isOnline = true;
|
||||
|
||||
constructor(public options: ReactNativeOpenPanelOptions) {
|
||||
super({
|
||||
...options,
|
||||
sdk: 'react-native',
|
||||
@@ -16,14 +60,30 @@ export class OpenPanel extends OpenPanelBase {
|
||||
});
|
||||
|
||||
this.api.addHeader('User-Agent', Constants.getWebViewUserAgentAsync());
|
||||
this.storage = options.storage;
|
||||
|
||||
if (options.networkInfo) {
|
||||
options.networkInfo.fetch().then(({ isConnected }) => {
|
||||
this.isOnline = isConnected ?? true;
|
||||
});
|
||||
options.networkInfo.addEventListener(({ isConnected }) => {
|
||||
const wasOffline = !this.isOnline;
|
||||
this.isOnline = isConnected ?? true;
|
||||
if (wasOffline && this.isOnline) {
|
||||
this.flush();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
AppState.addEventListener('change', (state) => {
|
||||
if (state === 'active') {
|
||||
this.setDefaultProperties();
|
||||
this.flush();
|
||||
}
|
||||
});
|
||||
|
||||
this.setDefaultProperties();
|
||||
this.loadPersistedQueue();
|
||||
}
|
||||
|
||||
private async setDefaultProperties() {
|
||||
@@ -37,6 +97,59 @@ export class OpenPanel extends OpenPanelBase {
|
||||
});
|
||||
}
|
||||
|
||||
private async loadPersistedQueue() {
|
||||
if (!this.storage) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const stored = await this.storage.getItem(QUEUE_STORAGE_KEY);
|
||||
if (stored) {
|
||||
const items = JSON.parse(stored);
|
||||
if (Array.isArray(items) && items.length > 0) {
|
||||
this.queue = [...items, ...this.queue];
|
||||
this.flush();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
this.log('Failed to load persisted queue');
|
||||
}
|
||||
}
|
||||
|
||||
private persistQueue() {
|
||||
if (!this.storage) {
|
||||
return;
|
||||
}
|
||||
this.storage
|
||||
.setItem(QUEUE_STORAGE_KEY, JSON.stringify(this.queue))
|
||||
.catch(() => {
|
||||
this.log('Failed to persist queue');
|
||||
});
|
||||
}
|
||||
|
||||
addQueue(payload: TrackHandlerPayload) {
|
||||
super.addQueue(payload);
|
||||
this.persistQueue();
|
||||
}
|
||||
|
||||
async send(payload: TrackHandlerPayload) {
|
||||
if (this.options.filter && !this.options.filter(payload)) {
|
||||
return null;
|
||||
}
|
||||
if (!this.isOnline) {
|
||||
this.addQueue(payload);
|
||||
return null;
|
||||
}
|
||||
return await super.send(payload);
|
||||
}
|
||||
|
||||
flush() {
|
||||
if (!this.isOnline) {
|
||||
return;
|
||||
}
|
||||
super.flush();
|
||||
this.persistQueue();
|
||||
}
|
||||
|
||||
track(name: string, properties?: TrackProperties) {
|
||||
return super.track(name, { ...properties, __path: this.lastPath });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user