Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dist-ssr
!.vscode/settings.json
.idea
.DS_Store
.expo/
*.suo
*.ntvs*
*.njsproj
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
**/gen
**/node_modules
**/dist
**/.expo
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"license": "MIT",
"workspaces": [
"packages/*",
"packages/react-sdk/dev/*",
"packages/react-native-sdk/dev/*",
"packages/openfeature-browser-provider/example"
],
"scripts": {
Expand All @@ -25,10 +25,16 @@
"devDependencies": {
"lerna": "^8.1.3",
"prettier": "^3.5.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"typedoc": "0.27.6",
"typedoc-plugin-frontmatter": "^1.1.2",
"typedoc-plugin-markdown": "^4.4.2",
"typedoc-plugin-mdn-links": "^4.0.7",
"typescript": "^5.7.3"
},
"resolutions": {
"react": "19.1.0",
"react-dom": "19.1.0"
}
}
5 changes: 4 additions & 1 deletion packages/browser-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"scripts": {
"dev": "vite",
"build": "tsc --project tsconfig.build.json && vite build",
"build": "tsc --project tsconfig.native.json && tsc --project tsconfig.build.json && vite build",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "yarn build && playwright test",
Expand All @@ -29,8 +29,10 @@
],
"main": "./dist/reflag-browser-sdk.umd.js",
"types": "./dist/types/src/index.d.ts",
"react-native": "./dist/index.native.js",
"exports": {
".": {
"react-native": "./dist/index.native.js",
"import": "./dist/reflag-browser-sdk.mjs",
"require": "./dist/reflag-browser-sdk.umd.js",
"types": "./dist/types/src/index.d.ts"
Expand Down Expand Up @@ -63,6 +65,7 @@
"typescript": "^5.7.3",
"vite": "^5.3.5",
"vite-plugin-dts": "^4.0.0-beta.1",
"vite-plugin-static-copy": "^1.0.6",
"vitest": "^2.0.4"
}
}
19 changes: 18 additions & 1 deletion packages/browser-sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { ReflagContext, ReflagDeprecatedContext } from "./context";
import { HookArgs, HooksManager, State } from "./hooksManager";
import { HttpClient } from "./httpClient";
import { Logger, loggerWithPrefix, quietConsoleLogger } from "./logger";
import { StorageAdapter } from "./storage";
import { showToolbarToggle } from "./toolbar";

const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
Expand Down Expand Up @@ -297,6 +298,12 @@ export type InitOptions = ReflagDeprecatedContext & {
* Pre-fetched flags to be used instead of fetching them from the server.
*/
bootstrappedFlags?: RawFlags;

/**
* Optional storage adapter used for caching flags and overrides.
* Useful for React Native (AsyncStorage).
*/
storage?: StorageAdapter;
};

const defaultConfig: Config = {
Expand Down Expand Up @@ -366,7 +373,9 @@ export interface Flag {

function shouldShowToolbar(opts: InitOptions) {
const toolbarOpts = opts.toolbar;
if (typeof window === "undefined") return false;
if (typeof window === "undefined" || typeof window.location === "undefined") {
return false;
}
if (typeof toolbarOpts === "boolean") return toolbarOpts;
if (typeof toolbarOpts?.show === "boolean") return toolbarOpts.show;
return window.location.hostname === "localhost";
Expand Down Expand Up @@ -441,6 +450,7 @@ export class ReflagClient {
timeoutMs: opts.timeoutMs,
fallbackFlags: opts.fallbackFlags,
offline: this.config.offline,
storage: opts.storage,
},
);

Expand Down Expand Up @@ -869,6 +879,13 @@ export class ReflagClient {
return this.flagsClient.getFlags();
}

/**
* Force refresh flags from the API, bypassing cache.
*/
refresh() {
return this.flagsClient.refreshFlags();
}

/**
* @deprecated Use `getFlag` instead.
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/browser-sdk/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export const SDK_VERSION = `browser-sdk/${version}`;
export const FLAG_EVENTS_PER_MIN = 1;
export const FLAGS_EXPIRE_MS = 30 * 24 * 60 * 60 * 1000; // expire entirely after 30 days

export const IS_SERVER = typeof window === "undefined";
export const IS_SERVER =
typeof window === "undefined" || typeof document === "undefined";
10 changes: 10 additions & 0 deletions packages/browser-sdk/src/feedback/ui/index.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { OpenFeedbackFormOptions } from "./types";

export function openFeedbackForm(_options: OpenFeedbackFormOptions): void {
// React Native doesn't support the web feedback UI.
// Users should implement their own UI and use `feedback` or `useSendFeedback`.
console.warn(
"[Reflag] Feedback UI is not supported in React Native. " +
"Use `feedback` or `useSendFeedback` with a custom UI instead.",
);
}
23 changes: 12 additions & 11 deletions packages/browser-sdk/src/flag/flagCache.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { StorageAdapter } from "../storage";

import { RawFlags } from "./flags";

interface StorageItem {
get(): string | null;
set(value: string): void;
}
const DEFAULT_STORAGE_KEY = "__reflag_fetched_flags";

interface cacheEntry {
expireAt: number;
Expand Down Expand Up @@ -52,7 +51,8 @@ export interface CacheResult {
}

export class FlagCache {
private storage: StorageItem;
private storage: StorageAdapter;
private readonly storageKey: string;
private readonly staleTimeMs: number;
private readonly expireTimeMs: number;

Expand All @@ -61,16 +61,17 @@ export class FlagCache {
staleTimeMs,
expireTimeMs,
}: {
storage: StorageItem;
storage: StorageAdapter;
staleTimeMs: number;
expireTimeMs: number;
}) {
this.storage = storage;
this.storageKey = DEFAULT_STORAGE_KEY;
this.staleTimeMs = staleTimeMs;
this.expireTimeMs = expireTimeMs;
}

set(
async set(
key: string,
{
flags,
Expand All @@ -81,7 +82,7 @@ export class FlagCache {
let cacheData: CacheData = {};

try {
const cachedResponseRaw = this.storage.get();
const cachedResponseRaw = await this.storage.getItem(this.storageKey);
if (cachedResponseRaw) {
cacheData = validateCacheData(JSON.parse(cachedResponseRaw)) ?? {};
}
Expand All @@ -99,14 +100,14 @@ export class FlagCache {
Object.entries(cacheData).filter(([_k, v]) => v.expireAt > Date.now()),
);

this.storage.set(JSON.stringify(cacheData));
await this.storage.setItem(this.storageKey, JSON.stringify(cacheData));

return cacheData;
}

get(key: string): CacheResult | undefined {
async get(key: string): Promise<CacheResult | undefined> {
try {
const cachedResponseRaw = this.storage.get();
const cachedResponseRaw = await this.storage.getItem(this.storageKey);
if (cachedResponseRaw) {
const cachedResponse = validateCacheData(JSON.parse(cachedResponseRaw));
if (
Expand Down
Loading
Loading