From af1f5757b18f706bd744ac25bcdf99292a654c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Voisin?= Date: Wed, 9 Aug 2023 17:34:58 +0200 Subject: [PATCH 1/5] [WIP] Remote config implementation --- firebase.json | 2 +- src/lib/components/RemoteConfig.svelte | 25 +++++++++++++ src/lib/stores/remote-config.ts | 50 ++++++++++++++++++++++++++ src/lib/stores/sdk.ts | 2 +- 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 src/lib/components/RemoteConfig.svelte create mode 100644 src/lib/stores/remote-config.ts diff --git a/firebase.json b/firebase.json index 47ec7d7..abea9fa 100644 --- a/firebase.json +++ b/firebase.json @@ -7,7 +7,7 @@ "port": 8080 }, "hosting": { - "port": 5000 + "port": 5001 }, "ui": { "enabled": true diff --git a/src/lib/components/RemoteConfig.svelte b/src/lib/components/RemoteConfig.svelte new file mode 100644 index 0000000..15d04c0 --- /dev/null +++ b/src/lib/components/RemoteConfig.svelte @@ -0,0 +1,25 @@ + + +{#if $store !== undefined} + +{:else} + +{/if} diff --git a/src/lib/stores/remote-config.ts b/src/lib/stores/remote-config.ts new file mode 100644 index 0000000..9264658 --- /dev/null +++ b/src/lib/stores/remote-config.ts @@ -0,0 +1,50 @@ +import { readable } from "svelte/store"; + +import { fetchAndActivate, getAll, isSupported, type RemoteConfig } from "firebase/remote-config"; + +/** + * @param {RemoteConfig} remoteConfig firebase remoteConfig instance + * @param {any} defaultValue optional default data. + * @returns a store with all remote config values + */ +export function configStore(remoteConfig: RemoteConfig, defaultValue: any = {}, minimumFetchIntervalMillis: number = 3600000) { + + // Fallback for SSR + if (!globalThis.window) { + const { subscribe } = readable(defaultValue); + return { + subscribe, + }; + } + + // Fallback for missing SDK + if (!remoteConfig) { + console.warn( + "Firebase RemoteConfig is not initialized. Are you missing FirebaseApp as a parent component?" + ); + const { subscribe } = readable(null); + return { + subscribe, + }; + } + + remoteConfig.settings.minimumFetchIntervalMillis = minimumFetchIntervalMillis; + remoteConfig.defaultConfig = defaultValue; + + + const { subscribe } = readable(defaultValue, (set) => { + isSupported().then((isSupported) => { + if (isSupported) { + fetchAndActivate(remoteConfig).then(() => { + set(getAll(remoteConfig)); + }).catch((err) => { + console.error(err); + }); + } + }) + }); + + return { + subscribe, + }; +} diff --git a/src/lib/stores/sdk.ts b/src/lib/stores/sdk.ts index 49eda6a..15a19db 100644 --- a/src/lib/stores/sdk.ts +++ b/src/lib/stores/sdk.ts @@ -1,6 +1,6 @@ -import { writable } from "svelte/store"; import type { Firestore } from "firebase/firestore"; import type { Auth } from "firebase/auth"; +import type { RemoteConfig } from "firebase/remote-config"; import { getContext, setContext } from "svelte"; From 49558bddd11422d4d3dd9fe5bf639b3cb928e040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Voisin?= Date: Thu, 17 Aug 2023 11:36:43 +0200 Subject: [PATCH 2/5] Adding client side support for remote config --- package-lock.json | 4 ++-- src/lib/components/RemoteConfig.svelte | 16 ++++++++++++---- src/lib/stores/remote-config.ts | 1 - 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index a82a5cb..e010b8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sveltefire", - "version": "0.3.0", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sveltefire", - "version": "0.3.0", + "version": "0.4.1", "devDependencies": { "@playwright/test": "^1.28.1", "@sveltejs/adapter-auto": "^2.0.0", diff --git a/src/lib/components/RemoteConfig.svelte b/src/lib/components/RemoteConfig.svelte index 15d04c0..9dbd618 100644 --- a/src/lib/components/RemoteConfig.svelte +++ b/src/lib/components/RemoteConfig.svelte @@ -1,15 +1,23 @@ + + + + + + +``` + +When initialized, you can use the `remoteConfig` store to access your remote configurations by using a `RemoteConfigBoolean`, `RemoteConfigNumber`, `RemoteConfigString` or `RemoteConfigValue`. + +### RemoteConfigBoolean, RemoteConfigNumber, RemoteConfigString + +Get a typed value from your RemoteConfig instance. + +```svelte + + + + +

Greeting text: {configValue}

+
+ + +

Is active? {configValue}

+
+ + +

Answer: {configValue}

+
+
+
+``` ## Using Components Together @@ -375,7 +430,5 @@ These components can be combined to build complex realtime apps. It's especially ## Roadmap -- Add support for Firebase Storage - Add support for Firebase RTDB - Add support for Firebase Analytics in SvelteKit -- Find a way to make TS generics with with Doc/Collection components diff --git a/src/lib/components/FirebaseApp.svelte b/src/lib/components/FirebaseApp.svelte index 3e5de55..52e8bb7 100644 --- a/src/lib/components/FirebaseApp.svelte +++ b/src/lib/components/FirebaseApp.svelte @@ -1,14 +1,16 @@ diff --git a/src/lib/components/RemoteConfig.svelte b/src/lib/components/RemoteConfig.svelte deleted file mode 100644 index 9dbd618..0000000 --- a/src/lib/components/RemoteConfig.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - -{#if $store !== undefined} - -{:else} - -{/if} diff --git a/src/lib/components/remote-config/RemoteConfig.svelte b/src/lib/components/remote-config/RemoteConfig.svelte new file mode 100644 index 0000000..396617f --- /dev/null +++ b/src/lib/components/remote-config/RemoteConfig.svelte @@ -0,0 +1,39 @@ + + +{#if remoteConfig !== undefined && $configActivated === true} + +{:else} + +{/if} diff --git a/src/lib/components/remote-config/RemoteConfigBoolean.svelte b/src/lib/components/remote-config/RemoteConfigBoolean.svelte new file mode 100644 index 0000000..b3133e0 --- /dev/null +++ b/src/lib/components/remote-config/RemoteConfigBoolean.svelte @@ -0,0 +1,21 @@ + + +{#if $store !== undefined} + +{:else} + +{/if} diff --git a/src/lib/components/remote-config/RemoteConfigNumber.svelte b/src/lib/components/remote-config/RemoteConfigNumber.svelte new file mode 100644 index 0000000..e949837 --- /dev/null +++ b/src/lib/components/remote-config/RemoteConfigNumber.svelte @@ -0,0 +1,23 @@ + + +{#if $store !== undefined} + +{:else} + +{/if} diff --git a/src/lib/components/remote-config/RemoteConfigString.svelte b/src/lib/components/remote-config/RemoteConfigString.svelte new file mode 100644 index 0000000..c165b94 --- /dev/null +++ b/src/lib/components/remote-config/RemoteConfigString.svelte @@ -0,0 +1,23 @@ + + +{#if $store !== undefined} + +{:else} + +{/if} diff --git a/src/lib/components/remote-config/RemoteConfigValue.svelte b/src/lib/components/remote-config/RemoteConfigValue.svelte new file mode 100644 index 0000000..bf6f0a1 --- /dev/null +++ b/src/lib/components/remote-config/RemoteConfigValue.svelte @@ -0,0 +1,21 @@ + + +{#if $store !== undefined} + +{:else} + +{/if} diff --git a/src/lib/stores/remote-config.ts b/src/lib/stores/remote-config.ts index c049a66..c0ba512 100644 --- a/src/lib/stores/remote-config.ts +++ b/src/lib/stores/remote-config.ts @@ -1,46 +1,144 @@ import { readable } from "svelte/store"; -import { fetchAndActivate, getAll, isSupported, type RemoteConfig } from "firebase/remote-config"; +import { fetchAndActivate, getBoolean, getNumber, getString, getValue, isSupported, type RemoteConfig } from "firebase/remote-config"; + /** * @param {RemoteConfig} remoteConfig firebase remoteConfig instance * @param {any} defaultValue optional default data. - * @returns a store with all remote config values + * @returns a store with the fallback remote config value as an object + */ +function fallbacks(remoteConfig: RemoteConfig, defaultValue: any | undefined = undefined){ + // Fallback for SSR + if (!globalThis.window) { + const { subscribe } = readable(defaultValue); + return { + subscribe, + }; + } + + // Fallback for missing SDK + if (!remoteConfig) { + console.warn( + "Firebase RemoteConfig is not initialized. Are you missing FirebaseApp as a parent component?" + ); + const { subscribe } = readable(null); + return { + subscribe, + }; + } +} + +/** + * @param {RemoteConfig} remoteConfig firebase remoteConfig instance + * @returns a store with the remote config activation status as a boolean +*/ +export function remoteConfigActivationStore(remoteConfig: RemoteConfig) { + + const fallbackValue = fallbacks(remoteConfig, undefined); + + if(fallbackValue){ + return fallbackValue; + } + + const { subscribe } = readable(undefined, (set) => { + isSupported().then(async (isSupported) => { + if (isSupported) { + fetchAndActivate(remoteConfig).then(() => { set(true) }); + } + }); + }); + + return { + subscribe, + }; +} + +/** + * @param {RemoteConfig} remoteConfig firebase remoteConfig instance + * @param {string} configKey the key of the remote config value + * @param {any} defaultValue optional default data. + * @returns a store with the requested remote config value as an object + */ +export function valueConfigStore(remoteConfig: RemoteConfig, configKey: string, defaultValue: any | undefined = undefined) { + + const fallbackValue = fallbacks(remoteConfig, defaultValue); + + if(fallbackValue){ + return fallbackValue; + } + + const { subscribe } = readable(defaultValue, (set) => { + set(getValue(remoteConfig, configKey)); + }); + + return { + subscribe, + }; +} + +/** + * @param {RemoteConfig} remoteConfig firebase remoteConfig instance + * @param {string} configKey the key of the remote config value + * @param {boolean} defaultValue optional default data. + * @returns a store with the requested remote config value as a boolean */ -export function configStore(remoteConfig: RemoteConfig, defaultValue: any = {}, minimumFetchIntervalMillis: number = 3600000) { - - // Fallback for SSR - if (!globalThis.window) { - const { subscribe } = readable(defaultValue); - return { - subscribe, - }; +export function booleanConfigStore(remoteConfig: RemoteConfig, configKey: string, defaultValue: boolean | undefined = undefined) { + + const fallbackValue = fallbacks(remoteConfig, defaultValue); + + if(fallbackValue){ + return fallbackValue; } - // Fallback for missing SDK - if (!remoteConfig) { - console.warn( - "Firebase RemoteConfig is not initialized. Are you missing FirebaseApp as a parent component?" - ); - const { subscribe } = readable(null); - return { - subscribe, - }; + const { subscribe } = readable(defaultValue, (set) => { + set(getBoolean(remoteConfig, configKey)); + }); + + return { + subscribe, + }; +} + +/** + * @param {RemoteConfig} remoteConfig firebase remoteConfig instance + * @param {string} configKey the key of the remote config value + * @param {string | undefined} defaultValue optional default data. + * @returns a store with the requested remote config value as a string + */ +export function stringConfigStore(remoteConfig: RemoteConfig, configKey: string, defaultValue: string | undefined = undefined) { + + const fallbackValue = fallbacks(remoteConfig, defaultValue); + + if(fallbackValue){ + return fallbackValue; } - remoteConfig.settings.minimumFetchIntervalMillis = minimumFetchIntervalMillis; - remoteConfig.defaultConfig = defaultValue; + const { subscribe } = readable(defaultValue, (set) => { + set(getString(remoteConfig, configKey)); + }); + + return { + subscribe, + }; +} + +/** + * @param {RemoteConfig} remoteConfig firebase remoteConfig instance + * @param {string} configKey the key of the remote config value + * @param {number | undefined} defaultValue optional default data. + * @returns a store with the requested remote config value as a number + */ +export function numberConfigStore(remoteConfig: RemoteConfig, configKey: string, defaultValue: number | undefined = undefined) { + + const fallbackValue = fallbacks(remoteConfig, defaultValue); + + if(fallbackValue){ + return fallbackValue; + } const { subscribe } = readable(defaultValue, (set) => { - isSupported().then((isSupported) => { - if (isSupported) { - fetchAndActivate(remoteConfig).then(() => { - set(getAll(remoteConfig)); - }).catch((err) => { - console.error(err); - }); - } - }) + set(getNumber(remoteConfig, configKey)); }); return { diff --git a/src/lib/stores/sdk.ts b/src/lib/stores/sdk.ts index 91d3f00..d8985f0 100644 --- a/src/lib/stores/sdk.ts +++ b/src/lib/stores/sdk.ts @@ -1,11 +1,11 @@ import type { Firestore } from "firebase/firestore"; import type { Auth } from "firebase/auth"; -import type { RemoteConfig } from "firebase/remote-config"; import { getContext, setContext } from "svelte"; import type { FirebaseStorage } from "firebase/storage"; - +import type { FirebaseApp } from "firebase/app"; export interface FirebaseSDKContext { + app?: FirebaseApp; auth?: Auth; firestore?: Firestore; storage?: FirebaseStorage; @@ -22,4 +22,4 @@ export function setFirebaseContext(sdks: FirebaseSDKContext) { */ export function getFirebaseContext(): FirebaseSDKContext { return getContext(contextKey); -} +} \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 3551237..fca9dea 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,9 +1,9 @@ - + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 258a113..098d590 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -13,8 +13,10 @@
  • Firestore Test
  • SSR Test
  • Storage Test
  • +
  • Remote Config Test
    • +
    • Firebase App Context: {!!ctx.app}
    • Auth Context: {!!ctx.auth}
    • Firestore Context: {!!ctx.firestore}
    • Storage Context: {!!ctx.storage}
    • diff --git a/src/routes/auth-test/+page.svelte b/src/routes/auth-test/+page.svelte index cacf096..113ac0c 100644 --- a/src/routes/auth-test/+page.svelte +++ b/src/routes/auth-test/+page.svelte @@ -13,6 +13,6 @@ -

      Signed Out

      +

      Signed Out

      diff --git a/src/routes/remote-config-test/+page.svelte b/src/routes/remote-config-test/+page.svelte new file mode 100644 index 0000000..c3d65ec --- /dev/null +++ b/src/routes/remote-config-test/+page.svelte @@ -0,0 +1,34 @@ + + +

      Remote Config Test

      + + + +

      Remote Config String: {configValue}

      +
      + + +

      Remote Config Boolean: {configValue}

      +
      + + +

      Remote Config Number: {configValue}

      +
      + + +

      Remote Config Value: {configValue.getSource()}

      +
      +
      \ No newline at end of file diff --git a/tests/auth.test.ts b/tests/auth.test.ts index 3f766c2..70ec345 100644 --- a/tests/auth.test.ts +++ b/tests/auth.test.ts @@ -16,9 +16,8 @@ test.describe.serial("Auth", () => { }); test("User can sign in and out", async () => { - await expect(page.getByRole("button", { name: "Sign In" })).toBeVisible(); - await page.getByRole("button", { name: "Sign In" }).click({delay: 1000}); + await page.getByRole("button", { name: "Sign In" }).click({ delay: 1000 }); await expect(page.getByRole("button", { name: "Sign Out" })).toBeVisible(); await page.getByRole("button", { name: "Sign Out" }).click(); diff --git a/tests/remote-config.test.ts b/tests/remote-config.test.ts new file mode 100644 index 0000000..b72acbb --- /dev/null +++ b/tests/remote-config.test.ts @@ -0,0 +1,21 @@ +import { expect, test, type Page } from "@playwright/test"; + +test.describe.serial("Remote Config", () => { + let page: Page; + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + await page.goto("/remote-config-test"); + }); + + test.afterAll(async () => { + await page.close(); + }); + + test("Displays default values", async () => { + await expect(page.getByTestId('string-config')).toContainText('Hello World'); + await expect(page.getByTestId('number-config')).toContainText('123.456'); + await expect(page.getByTestId('value-config')).toContainText('default'); + await expect(page.getByTestId('boolean-config')).toContainText('true'); + }); +}); From 4e295e57e9817cfae8e4ad245b49cf365039b7c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Voisin?= Date: Fri, 18 Aug 2023 22:29:10 +0200 Subject: [PATCH 4/5] Documentation --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3f3ce26..595f239 100644 --- a/README.md +++ b/README.md @@ -344,6 +344,7 @@ This components takes care of intializing the RemoteConfig instance from a clien ```svelte