From bd7352c475ae85f96c8a53587b50410478a0d82d Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Wed, 18 Feb 2026 23:03:24 +0100 Subject: [PATCH 1/7] Add bare RN smoke app and fix RN runtime compatibility --- packages/browser-sdk/src/httpClient.ts | 9 +- packages/browser-sdk/test/httpClient.test.ts | 33 + .../react-native-sdk/dev/bare-rn/.eslintrc.js | 4 + .../react-native-sdk/dev/bare-rn/.gitignore | 76 + .../dev/bare-rn/.prettierrc.js | 5 + .../dev/bare-rn/.watchmanconfig | 1 + packages/react-native-sdk/dev/bare-rn/App.tsx | 241 ++ packages/react-native-sdk/dev/bare-rn/Gemfile | 17 + .../react-native-sdk/dev/bare-rn/Gemfile.lock | 172 ++ .../react-native-sdk/dev/bare-rn/README.md | 63 + .../dev/bare-rn/android/app/build.gradle | 119 + .../dev/bare-rn/android/app/debug.keystore | Bin 0 -> 2257 bytes .../bare-rn/android/app/proguard-rules.pro | 10 + .../android/app/src/main/AndroidManifest.xml | 27 + .../com/reflagbarernsmoke/MainActivity.kt | 22 + .../com/reflagbarernsmoke/MainApplication.kt | 38 + .../res/drawable/rn_edit_text_material.xml | 37 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 9 + .../dev/bare-rn/android/build.gradle | 21 + .../dev/bare-rn/android/gradle.properties | 44 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + .../dev/bare-rn/android/gradlew | 251 ++ .../dev/bare-rn/android/gradlew.bat | 99 + .../dev/bare-rn/android/settings.gradle | 6 + .../react-native-sdk/dev/bare-rn/app.json | 4 + .../dev/bare-rn/babel.config.js | 3 + .../react-native-sdk/dev/bare-rn/index.js | 9 + .../dev/bare-rn/ios/.xcode.env | 11 + .../react-native-sdk/dev/bare-rn/ios/Podfile | 35 + .../dev/bare-rn/ios/Podfile.lock | 2682 +++++++++++++++++ .../project.pbxproj | 478 +++ .../xcschemes/ReflagBareRnSmoke.xcscheme | 88 + .../contents.xcworkspacedata | 10 + .../ios/ReflagBareRnSmoke/AppDelegate.swift | 48 + .../AppIcon.appiconset/Contents.json | 53 + .../Images.xcassets/Contents.json | 6 + .../bare-rn/ios/ReflagBareRnSmoke/Info.plist | 53 + .../ReflagBareRnSmoke/LaunchScreen.storyboard | 47 + .../ReflagBareRnSmoke/PrivacyInfo.xcprivacy | 37 + .../dev/bare-rn/metro.config.js | 24 + .../react-native-sdk/dev/bare-rn/package.json | 37 + .../dev/bare-rn/tsconfig.json | 5 + packages/react-native-sdk/package.json | 12 +- yarn.lock | 686 ++++- 56 files changed, 5620 insertions(+), 22 deletions(-) create mode 100644 packages/react-native-sdk/dev/bare-rn/.eslintrc.js create mode 100644 packages/react-native-sdk/dev/bare-rn/.gitignore create mode 100644 packages/react-native-sdk/dev/bare-rn/.prettierrc.js create mode 100644 packages/react-native-sdk/dev/bare-rn/.watchmanconfig create mode 100644 packages/react-native-sdk/dev/bare-rn/App.tsx create mode 100644 packages/react-native-sdk/dev/bare-rn/Gemfile create mode 100644 packages/react-native-sdk/dev/bare-rn/Gemfile.lock create mode 100644 packages/react-native-sdk/dev/bare-rn/README.md create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/build.gradle create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/debug.keystore create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/proguard-rules.pro create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/AndroidManifest.xml create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/java/com/reflagbarernsmoke/MainActivity.kt create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/java/com/reflagbarernsmoke/MainApplication.kt create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/res/drawable/rn_edit_text_material.xml create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/res/values/strings.xml create mode 100644 packages/react-native-sdk/dev/bare-rn/android/app/src/main/res/values/styles.xml create mode 100644 packages/react-native-sdk/dev/bare-rn/android/build.gradle create mode 100644 packages/react-native-sdk/dev/bare-rn/android/gradle.properties create mode 100644 packages/react-native-sdk/dev/bare-rn/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 packages/react-native-sdk/dev/bare-rn/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 packages/react-native-sdk/dev/bare-rn/android/gradlew create mode 100644 packages/react-native-sdk/dev/bare-rn/android/gradlew.bat create mode 100644 packages/react-native-sdk/dev/bare-rn/android/settings.gradle create mode 100644 packages/react-native-sdk/dev/bare-rn/app.json create mode 100644 packages/react-native-sdk/dev/bare-rn/babel.config.js create mode 100644 packages/react-native-sdk/dev/bare-rn/index.js create mode 100644 packages/react-native-sdk/dev/bare-rn/ios/.xcode.env create mode 100644 packages/react-native-sdk/dev/bare-rn/ios/Podfile create mode 100644 packages/react-native-sdk/dev/bare-rn/ios/Podfile.lock create mode 100644 packages/react-native-sdk/dev/bare-rn/ios/ReflagBareRnSmoke.xcodeproj/project.pbxproj create mode 100644 packages/react-native-sdk/dev/bare-rn/ios/ReflagBareRnSmoke.xcodeproj/xcshareddata/xcschemes/ReflagBareRnSmoke.xcscheme create mode 100644 packages/react-native-sdk/dev/bare-rn/ios/ReflagBareRnSmoke.xcworkspace/contents.xcworkspacedata create mode 100644 packages/react-native-sdk/dev/bare-rn/ios/ReflagBareRnSmoke/AppDelegate.swift create mode 100644 packages/react-native-sdk/dev/bare-rn/ios/ReflagBareRnSmoke/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/react-native-sdk/dev/bare-rn/ios/ReflagBareRnSmoke/Images.xcassets/Contents.json create mode 100644 packages/react-native-sdk/dev/bare-rn/ios/ReflagBareRnSmoke/Info.plist create mode 100644 packages/react-native-sdk/dev/bare-rn/ios/ReflagBareRnSmoke/LaunchScreen.storyboard create mode 100644 packages/react-native-sdk/dev/bare-rn/ios/ReflagBareRnSmoke/PrivacyInfo.xcprivacy create mode 100644 packages/react-native-sdk/dev/bare-rn/metro.config.js create mode 100644 packages/react-native-sdk/dev/bare-rn/package.json create mode 100644 packages/react-native-sdk/dev/bare-rn/tsconfig.json diff --git a/packages/browser-sdk/src/httpClient.ts b/packages/browser-sdk/src/httpClient.ts index 14f584c3..bf9f207a 100644 --- a/packages/browser-sdk/src/httpClient.ts +++ b/packages/browser-sdk/src/httpClient.ts @@ -51,8 +51,13 @@ export class HttpClient { params.set(SDK_VERSION_HEADER_NAME, this.sdkVersion); params.set("publishableKey", this.publishableKey); - const url = this.getUrl(path); - url.search = params.toString(); + // Do not assign `url.search` directly: some React Native URL implementations + // expose `search` as getter-only and throw at runtime on assignment. + const query = params.toString(); + const pathWithQuery = query + ? `${path}${path.includes("?") ? "&" : "?"}${query}` + : path; + const url = this.getUrl(pathWithQuery); if (timeoutMs === undefined) { return fetch(url, this.fetchOptions); diff --git a/packages/browser-sdk/test/httpClient.test.ts b/packages/browser-sdk/test/httpClient.test.ts index a885d99d..9389b5ec 100644 --- a/packages/browser-sdk/test/httpClient.test.ts +++ b/packages/browser-sdk/test/httpClient.test.ts @@ -63,4 +63,37 @@ describe("sets `credentials`", () => { expect.objectContaining({ credentials: "include" }), ); }); + + test("does not require a writable `URL.search` property", async () => { + const OriginalURL = global.URL; + + class ReadonlySearchURL extends OriginalURL { + get search() { + return super.search; + } + + set search(_value: string) { + throw new TypeError( + "Cannot assign to property 'search' which has only a getter", + ); + } + } + + global.URL = ReadonlySearchURL as unknown as typeof URL; + + try { + const client = new HttpClient("publishableKey"); + await client.get({ + path: "/test", + params: new URLSearchParams({ hello: "world" }), + }); + + const [firstArg] = vi.mocked(global.fetch).mock.calls[0]!; + const url = new OriginalURL(String(firstArg)); + expect(url.searchParams.get("hello")).toBe("world"); + expect(url.searchParams.get("publishableKey")).toBe("publishableKey"); + } finally { + global.URL = OriginalURL; + } + }); }); diff --git a/packages/react-native-sdk/dev/bare-rn/.eslintrc.js b/packages/react-native-sdk/dev/bare-rn/.eslintrc.js new file mode 100644 index 00000000..187894b6 --- /dev/null +++ b/packages/react-native-sdk/dev/bare-rn/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: '@react-native', +}; diff --git a/packages/react-native-sdk/dev/bare-rn/.gitignore b/packages/react-native-sdk/dev/bare-rn/.gitignore new file mode 100644 index 00000000..beacfa68 --- /dev/null +++ b/packages/react-native-sdk/dev/bare-rn/.gitignore @@ -0,0 +1,76 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +**/.xcode.env.local + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +.cxx/ +*.keystore +!debug.keystore +.kotlin/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +**/fastlane/report.xml +**/fastlane/Preview.html +**/fastlane/screenshots +**/fastlane/test_output + +# Bundle artifact +*.jsbundle + +# Ruby / CocoaPods +**/Pods/ +/vendor/bundle/ +.bundle/ + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* + +# testing +/coverage + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions diff --git a/packages/react-native-sdk/dev/bare-rn/.prettierrc.js b/packages/react-native-sdk/dev/bare-rn/.prettierrc.js new file mode 100644 index 00000000..06860c8d --- /dev/null +++ b/packages/react-native-sdk/dev/bare-rn/.prettierrc.js @@ -0,0 +1,5 @@ +module.exports = { + arrowParens: 'avoid', + singleQuote: true, + trailingComma: 'all', +}; diff --git a/packages/react-native-sdk/dev/bare-rn/.watchmanconfig b/packages/react-native-sdk/dev/bare-rn/.watchmanconfig new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/packages/react-native-sdk/dev/bare-rn/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/packages/react-native-sdk/dev/bare-rn/App.tsx b/packages/react-native-sdk/dev/bare-rn/App.tsx new file mode 100644 index 00000000..50114d09 --- /dev/null +++ b/packages/react-native-sdk/dev/bare-rn/App.tsx @@ -0,0 +1,241 @@ +import React, { useMemo } from "react"; +import { + Button, + ScrollView, + StatusBar, + StyleSheet, + Text, + View, +} from "react-native"; +import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; +import { + ReflagProvider, + useClient, + useFlag, + useIsLoading, +} from "@reflag/react-native-sdk"; + +const publishableKey = "pub_prod_vxuM5hSZOnhzvAfiOnZ9rj"; +const isConfigured = publishableKey.length > 0; + +type CheckStatus = "pass" | "warn" | "fail"; + +interface RuntimeCheck { + label: string; + status: CheckStatus; + details: string; +} + +function runRuntimeChecks(): RuntimeCheck[] { + const checks: RuntimeCheck[] = []; + + const hasURL = typeof URL !== "undefined"; + checks.push({ + label: "global URL", + status: hasURL ? "pass" : "fail", + details: hasURL ? "available" : "missing", + }); + + const hasURLSearchParams = typeof URLSearchParams !== "undefined"; + checks.push({ + label: "global URLSearchParams", + status: hasURLSearchParams ? "pass" : "fail", + details: hasURLSearchParams ? "available" : "missing", + }); + + if (hasURL) { + const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, "search"); + checks.push({ + label: "URL.search setter", + status: descriptor?.set ? "pass" : "warn", + details: descriptor?.set + ? "setter present" + : "getter-only URL.search (older RN behavior)", + }); + } + + try { + const url = new URL("/probe", "https://front.reflag.com"); + url.searchParams.set("check", "ok"); + const href = url.toString(); + checks.push({ + label: "URL + searchParams behavior", + status: href.includes("check=ok") ? "pass" : "fail", + details: href, + }); + } catch (error) { + checks.push({ + label: "URL + searchParams behavior", + status: "fail", + details: String(error), + }); + } + + checks.push({ + label: "global EventSource (auto feedback only)", + status: typeof EventSource !== "undefined" ? "pass" : "warn", + details: + typeof EventSource !== "undefined" + ? "available" + : "missing: keep feedback.enableAutoFeedback=false", + }); + + return checks; +} + +function RuntimeChecksCard() { + const checks = useMemo(() => runRuntimeChecks(), []); + + return ( + + Runtime Checks + {checks.map((check) => ( + + + {check.status.toUpperCase()} + + + {check.label} + {check.details} + + + ))} + + ); +} + +function FlagCard() { + const client = useClient(); + const isLoading = useIsLoading(); + const { isEnabled, track } = useFlag("bare-rn-demo"); + + return ( + + Flag: bare-rn-demo + + Status: {isLoading ? "loading" : isEnabled ? "enabled" : "disabled"} + + +