Skip to content

Commit

Permalink
2.2.0
Browse files Browse the repository at this point in the history
Added an OpenFeature client provider for the React SDK to manage pricing-driven feature flags.
  • Loading branch information
Alex-GF authored Jan 26, 2025
2 parents 14288c1 + 8b3efc2 commit 9c8f5a6
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 14 deletions.
37 changes: 31 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 6 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
142 changes: 142 additions & 0 deletions src/provider/OpenFeatureProvider.ts
Original file line number Diff line number Diff line change
@@ -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<string, ExtendedFeatureStatus> = {};

onContextChange(oldContext: any, newContext: any): Promise<void> | void {
this.evaluation = evaluatePricing(
this.pricingYaml!,
newContext.subscription,
newContext.userContext!
);
}

resolveBooleanEvaluation(flagKey: string, defaultValue: boolean): ResolutionDetails<boolean> {
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<string> {
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<number> {
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<T extends JsonValue>(
flagKey: string,
defaultValue: T
): ResolutionDetails<T> {
try {
return this._evaluateFeature(flagKey) as unknown as ResolutionDetails<T>;
} catch (error) {
console.error('Error occurred during evaluation. ERROR: ', (error as Error).message);
return {
value: defaultValue,
} as ResolutionDetails<T>;
}
}

initialize?(context?: any): Promise<void> {
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<any> {
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],
};
}
}
}

0 comments on commit 9c8f5a6

Please sign in to comment.