Skip to content

Commit 3e9187b

Browse files
committed
Add refresh control and RN fixes
1 parent 2f4574f commit 3e9187b

30 files changed

Lines changed: 5027 additions & 254 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dist-ssr
2020
!.vscode/settings.json
2121
.idea
2222
.DS_Store
23+
.expo/
2324
*.suo
2425
*.ntvs*
2526
*.njsproj

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,16 @@
2525
"devDependencies": {
2626
"lerna": "^8.1.3",
2727
"prettier": "^3.5.2",
28+
"react": "19.1.0",
29+
"react-dom": "19.1.0",
2830
"typedoc": "0.27.6",
2931
"typedoc-plugin-frontmatter": "^1.1.2",
3032
"typedoc-plugin-markdown": "^4.4.2",
3133
"typedoc-plugin-mdn-links": "^4.0.7",
3234
"typescript": "^5.7.3"
35+
},
36+
"resolutions": {
37+
"react": "19.1.0",
38+
"react-dom": "19.1.0"
3339
}
3440
}

packages/browser-sdk/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"scripts": {
1414
"dev": "vite",
15-
"build": "tsc --project tsconfig.build.json && vite build",
15+
"build": "tsc --project tsconfig.build.json && vite build && node scripts/create-native-entry.cjs",
1616
"test": "vitest run",
1717
"test:watch": "vitest",
1818
"test:e2e": "yarn build && playwright test",
@@ -29,8 +29,10 @@
2929
],
3030
"main": "./dist/reflag-browser-sdk.umd.js",
3131
"types": "./dist/types/src/index.d.ts",
32+
"react-native": "./dist/index.native.js",
3233
"exports": {
3334
".": {
35+
"react-native": "./dist/index.native.js",
3436
"import": "./dist/reflag-browser-sdk.mjs",
3537
"require": "./dist/reflag-browser-sdk.umd.js",
3638
"types": "./dist/types/src/index.d.ts"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
4+
const distDir = path.join(__dirname, "..", "dist");
5+
const outFile = path.join(distDir, "index.native.js");
6+
7+
if (!fs.existsSync(distDir)) {
8+
fs.mkdirSync(distDir, { recursive: true });
9+
}
10+
11+
const contents = `"use strict";\nmodule.exports = require("./reflag-browser-sdk.umd.js");\n`;
12+
fs.writeFileSync(outFile, contents);
13+
14+
console.log("Wrote", outFile);

packages/browser-sdk/src/client.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,14 @@ import { ReflagContext, ReflagDeprecatedContext } from "./context";
2626
import { HookArgs, HooksManager, State } from "./hooksManager";
2727
import { HttpClient } from "./httpClient";
2828
import { Logger, loggerWithPrefix, quietConsoleLogger } from "./logger";
29+
import { StorageAdapter } from "./storage";
2930
import { showToolbarToggle } from "./toolbar";
3031

3132
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
3233
const isNode = typeof document === "undefined"; // deno supports "window" but not "document" according to https://remix.run/docs/en/main/guides/gotchas
34+
const isReactNative =
35+
typeof navigator !== "undefined" &&
36+
/ReactNative/i.test(navigator.userAgent ?? "");
3337

3438
/**
3539
* (Internal) User context.
@@ -297,6 +301,12 @@ export type InitOptions = ReflagDeprecatedContext & {
297301
* Pre-fetched flags to be used instead of fetching them from the server.
298302
*/
299303
bootstrappedFlags?: RawFlags;
304+
305+
/**
306+
* Optional storage adapter used for caching flags and overrides.
307+
* Useful for React Native (AsyncStorage).
308+
*/
309+
storage?: StorageAdapter;
300310
};
301311

302312
const defaultConfig: Config = {
@@ -366,7 +376,9 @@ export interface Flag {
366376

367377
function shouldShowToolbar(opts: InitOptions) {
368378
const toolbarOpts = opts.toolbar;
369-
if (typeof window === "undefined") return false;
379+
if (typeof window === "undefined" || typeof window.location === "undefined") {
380+
return false;
381+
}
370382
if (typeof toolbarOpts === "boolean") return toolbarOpts;
371383
if (typeof toolbarOpts?.show === "boolean") return toolbarOpts.show;
372384
return window.location.hostname === "localhost";
@@ -439,13 +451,15 @@ export class ReflagClient {
439451
timeoutMs: opts.timeoutMs,
440452
fallbackFlags: opts.fallbackFlags,
441453
offline: this.config.offline,
454+
storage: opts.storage,
442455
},
443456
);
444457

445458
if (
446459
!this.config.offline &&
447460
this.context?.user &&
448461
!isNode && // do not prompt on server-side
462+
!isReactNative && // disable SSE-based auto feedback in React Native
449463
opts?.feedback?.enableAutoFeedback !== false // default to on
450464
) {
451465
if (isMobile) {
@@ -870,6 +884,13 @@ export class ReflagClient {
870884
return this.flagsClient.getFlags();
871885
}
872886

887+
/**
888+
* Force refresh flags from the API, bypassing cache.
889+
*/
890+
refresh() {
891+
return this.flagsClient.refreshFlags();
892+
}
893+
873894
/**
874895
* @deprecated Use `getFlag` instead.
875896
*/

packages/browser-sdk/src/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export const SDK_VERSION = `browser-sdk/${version}`;
1010
export const FLAG_EVENTS_PER_MIN = 1;
1111
export const FLAGS_EXPIRE_MS = 30 * 24 * 60 * 60 * 1000; // expire entirely after 30 days
1212

13-
export const IS_SERVER = typeof window === "undefined";
13+
export const IS_SERVER =
14+
typeof window === "undefined" || typeof document === "undefined";
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Position } from "../../ui/types";
2+
3+
import { OpenFeedbackFormOptions } from "./types";
4+
5+
export const DEFAULT_POSITION: Position = {
6+
type: "DIALOG",
7+
placement: "bottom-right",
8+
};
9+
10+
export function openFeedbackForm(_options: OpenFeedbackFormOptions): void {
11+
// React Native doesn't support the web feedback UI.
12+
// Users should implement their own UI and use `feedback` or `useSendFeedback`.
13+
console.warn(
14+
"[Reflag] Feedback UI is not supported in React Native. " +
15+
"Use `feedback` or `useSendFeedback` with a custom UI instead.",
16+
);
17+
}

packages/browser-sdk/src/flag/flagCache.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import { RawFlags } from "./flags";
1+
import { StorageAdapter } from "../storage";
22

3-
interface StorageItem {
4-
get(): string | null;
5-
set(value: string): void;
6-
}
3+
import { RawFlags } from "./flags";
74

85
interface cacheEntry {
96
expireAt: number;
@@ -52,25 +49,29 @@ export interface CacheResult {
5249
}
5350

5451
export class FlagCache {
55-
private storage: StorageItem;
52+
private storage: StorageAdapter | null;
53+
private readonly storageKey: string;
5654
private readonly staleTimeMs: number;
5755
private readonly expireTimeMs: number;
5856

5957
constructor({
6058
storage,
59+
storageKey,
6160
staleTimeMs,
6261
expireTimeMs,
6362
}: {
64-
storage: StorageItem;
63+
storage: StorageAdapter | null;
64+
storageKey: string;
6565
staleTimeMs: number;
6666
expireTimeMs: number;
6767
}) {
6868
this.storage = storage;
69+
this.storageKey = storageKey;
6970
this.staleTimeMs = staleTimeMs;
7071
this.expireTimeMs = expireTimeMs;
7172
}
7273

73-
set(
74+
async set(
7475
key: string,
7576
{
7677
flags,
@@ -81,7 +82,8 @@ export class FlagCache {
8182
let cacheData: CacheData = {};
8283

8384
try {
84-
const cachedResponseRaw = this.storage.get();
85+
if (!this.storage) return cacheData;
86+
const cachedResponseRaw = await this.storage.getItem(this.storageKey);
8587
if (cachedResponseRaw) {
8688
cacheData = validateCacheData(JSON.parse(cachedResponseRaw)) ?? {};
8789
}
@@ -99,14 +101,16 @@ export class FlagCache {
99101
Object.entries(cacheData).filter(([_k, v]) => v.expireAt > Date.now()),
100102
);
101103

102-
this.storage.set(JSON.stringify(cacheData));
104+
if (!this.storage) return cacheData;
105+
await this.storage.setItem(this.storageKey, JSON.stringify(cacheData));
103106

104107
return cacheData;
105108
}
106109

107-
get(key: string): CacheResult | undefined {
110+
async get(key: string): Promise<CacheResult | undefined> {
108111
try {
109-
const cachedResponseRaw = this.storage.get();
112+
if (!this.storage) return;
113+
const cachedResponseRaw = await this.storage.getItem(this.storageKey);
110114
if (cachedResponseRaw) {
111115
const cachedResponse = validateCacheData(JSON.parse(cachedResponseRaw));
112116
if (

0 commit comments

Comments
 (0)