From 8b3efc2da548f6989238e478b05987f24690bc91 Mon Sep 17 00:00:00 2001 From: Alex-GF Date: Sun, 26 Jan 2025 13:52:42 +0100 Subject: [PATCH] feat: OpenFeature provider --- package-lock.json | 37 ++++++-- package.json | 13 ++- src/index.ts | 7 +- src/provider/OpenFeatureProvider.ts | 142 ++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 14 deletions(-) create mode 100644 src/provider/OpenFeatureProvider.ts diff --git a/package-lock.json b/package-lock.json index 7c6a7a1..10ba2f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "pricing4react", - "version": "2.0.1", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pricing4react", - "version": "2.0.1", + "version": "2.1.0", "license": "MIT", "dependencies": { + "@openfeature/react-sdk": "^0.4.10", "buffer": "^6.0.3", "pricing4ts": "^0.7.1" }, @@ -969,6 +970,33 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@openfeature/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@openfeature/core/-/core-1.6.0.tgz", + "integrity": "sha512-QYAtwdreZU9Mi/LXLRzXsUA7PhbtT7+UJfRBMIAy6MidZjMgIbNfoh6+MncXb3UocThn0OsYa8WLfWD9q43eCQ==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@openfeature/react-sdk": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/@openfeature/react-sdk/-/react-sdk-0.4.10.tgz", + "integrity": "sha512-t0DU5tR7s6Z3Oi3nbiWgdSuOs1hN9NH28fcnb3vvYdILetY3tCuhUI07xedzGsU6LCbOgNr65a9sEtTjrXjb5w==", + "license": "Apache-2.0", + "peerDependencies": { + "@openfeature/web-sdk": "^1.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@openfeature/web-sdk": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@openfeature/web-sdk/-/web-sdk-1.4.0.tgz", + "integrity": "sha512-cMCt5jszLiZ9mLacS7XjMTEpbIS3asttSpyrPJ8rAdwDk86UjzfPwzMTSiccVolJqS299hWGXC1FGbu4IHX40Q==", + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@openfeature/core": "^1.6.0" + } + }, "node_modules/@remix-run/router": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.8.0.tgz", @@ -3639,8 +3667,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -3761,7 +3788,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -4742,7 +4768,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0" }, diff --git a/package.json b/package.json index b4b6503..15d922c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pricing4react", - "version": "2.1.0", + "version": "2.2.0", "description": "A library of components that ease the integration of feature toggling driven by pricing plans into your React application's UI.", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -25,22 +25,21 @@ }, "license": "MIT", "dependencies": { + "@openfeature/react-sdk": "^0.4.10", + "@types/react": "^18.2.12", "buffer": "^6.0.3", - "pricing4ts": "^0.7.1" + "pricing4ts": "^0.7.1", + "react": ">=18.2.0", + "react-dom": ">=18.2.0" }, "peerDependencies": { - "react": ">=18.2.0", - "react-dom": ">=18.2.0", "react-router-dom": ">=6.15.0" }, "devDependencies": { "@types/jest": "^29.5.14", - "@types/react": "^18.2.12", "@types/react-dom": "^18.2.7", "cssnano": "^6.0.2", "jest": "^29.7.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", "react-router-dom": "^6.15.0", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", diff --git a/src/index.ts b/src/index.ts index a4cc068..8a8fb5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,4 +21,9 @@ export { searchNewTokenAndUpdate, } from "./services/api.service"; -export { evaluateFeatureInPricing, evaluatePricing } from "./services/evaluation.service"; +export { + evaluateFeatureInPricing, + evaluatePricing, +} from "./services/evaluation.service"; + +export { ReactPricingDrivenFeaturesProvider } from "./provider/OpenFeatureProvider"; diff --git a/src/provider/OpenFeatureProvider.ts b/src/provider/OpenFeatureProvider.ts new file mode 100644 index 0000000..cf52584 --- /dev/null +++ b/src/provider/OpenFeatureProvider.ts @@ -0,0 +1,142 @@ +import { + ClientProviderStatus, + Hook, + JsonValue, + OpenFeatureEventEmitter, + Provider, + ResolutionDetails, + } from '@openfeature/react-sdk'; + import { evaluatePricing } from "../index"; + import { ExtendedFeatureStatus } from 'pricing4ts'; + + export class ReactPricingDrivenFeaturesProvider implements Provider { + readonly metadata = { + name: 'pricing-driven-features', + description: 'An Open Feature provider that enables features based on pricing information', + }; + + readonly runsOn = 'client'; + + events = new OpenFeatureEventEmitter(); + hooks?: Hook[] | undefined; + status?: ClientProviderStatus | undefined; + + private pricingUrl: string | undefined; + private pricingYaml: string | undefined; + private evaluation: Record = {}; + + onContextChange(oldContext: any, newContext: any): Promise | void { + this.evaluation = evaluatePricing( + this.pricingYaml!, + newContext.subscription, + newContext.userContext! + ); + } + + resolveBooleanEvaluation(flagKey: string, defaultValue: boolean): ResolutionDetails { + try { + return { + value: this._evaluateFeature(flagKey).value.eval as boolean, + }; + } catch (error) { + console.error('Error occurred during evaluation. ERROR: ', (error as Error).message); + return { + value: defaultValue, + }; + } + } + + resolveStringEvaluation(flagKey: string, defaultValue: string): ResolutionDetails { + try { + const result = this._evaluateFeature(flagKey); + return { + value: result.value.eval.toString(), + }; + } catch (error) { + console.error('Error occurred during evaluation. ERROR: ', (error as Error).message); + return { + value: defaultValue, + }; + } + } + + resolveNumberEvaluation(flagKey: string, defaultValue: number): ResolutionDetails { + try { + const result = this._evaluateFeature(flagKey); + return { + value: result.value.eval ? 1 : 0, + }; + } catch (error) { + console.error('Error occurred during evaluation. ERROR: ', (error as Error).message); + return { + value: defaultValue, + }; + } + } + + resolveObjectEvaluation( + flagKey: string, + defaultValue: T + ): ResolutionDetails { + try { + return this._evaluateFeature(flagKey) as unknown as ResolutionDetails; + } catch (error) { + console.error('Error occurred during evaluation. ERROR: ', (error as Error).message); + return { + value: defaultValue, + } as ResolutionDetails; + } + } + + initialize?(context?: any): Promise { + if (context.pricingUrl) { + this.pricingUrl = context.pricingUrl; + if (!context.subscription) { + return Promise.reject( + "Subscription not provided in context. Use 'subscription' to provide one. It's value must be a list comprised of at least one name of plan/add-on." + ); + } + if (!context.userContext) { + return Promise.reject( + "User context not provided in context. Use 'userContext' to provide one. It's value must be a JSON object with at least the key 'user', with a string value identifying the user." + ); + } + return fetch(this.pricingUrl!) + .then(response => response.text()) + .then(yaml => { + this.pricingYaml = yaml; + this.evaluation = evaluatePricing( + this.pricingYaml, + context.subscription, + context.userContext! + ); + console.log("PricingDrivenFeaturesProvider initialized with context: ", context); + }); + } else { + return Promise.reject( + "Pricing URL not provided in context. Use 'pricingUrl' to provide one." + ); + } + } + + private _evaluateFeature(flagKey: string): ResolutionDetails { + if (Object.keys(this.evaluation).length === 0) { + return { + value: { + eval: false, + used: null, + limit: null, + error: { + code: 'PROVIDER_NOT_READY', + message: 'Pricing not yet loaded into the provider', + }, + }, + }; + } else { + return { + value: this.evaluation[flagKey], + }; + } + } + } + \ No newline at end of file