Skip to content

Commit 8b3efc2

Browse files
committed
feat: OpenFeature provider
1 parent c5fa87c commit 8b3efc2

File tree

4 files changed

+185
-14
lines changed

4 files changed

+185
-14
lines changed

package-lock.json

Lines changed: 31 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pricing4react",
3-
"version": "2.1.0",
3+
"version": "2.2.0",
44
"description": "A library of components that ease the integration of feature toggling driven by pricing plans into your React application's UI.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -25,22 +25,21 @@
2525
},
2626
"license": "MIT",
2727
"dependencies": {
28+
"@openfeature/react-sdk": "^0.4.10",
29+
"@types/react": "^18.2.12",
2830
"buffer": "^6.0.3",
29-
"pricing4ts": "^0.7.1"
31+
"pricing4ts": "^0.7.1",
32+
"react": ">=18.2.0",
33+
"react-dom": ">=18.2.0"
3034
},
3135
"peerDependencies": {
32-
"react": ">=18.2.0",
33-
"react-dom": ">=18.2.0",
3436
"react-router-dom": ">=6.15.0"
3537
},
3638
"devDependencies": {
3739
"@types/jest": "^29.5.14",
38-
"@types/react": "^18.2.12",
3940
"@types/react-dom": "^18.2.7",
4041
"cssnano": "^6.0.2",
4142
"jest": "^29.7.0",
42-
"react": "^18.2.0",
43-
"react-dom": "^18.2.0",
4443
"react-router-dom": "^6.15.0",
4544
"ts-jest": "^29.2.5",
4645
"ts-node": "^10.9.2",

src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,9 @@ export {
2121
searchNewTokenAndUpdate,
2222
} from "./services/api.service";
2323

24-
export { evaluateFeatureInPricing, evaluatePricing } from "./services/evaluation.service";
24+
export {
25+
evaluateFeatureInPricing,
26+
evaluatePricing,
27+
} from "./services/evaluation.service";
28+
29+
export { ReactPricingDrivenFeaturesProvider } from "./provider/OpenFeatureProvider";

src/provider/OpenFeatureProvider.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import {
2+
ClientProviderStatus,
3+
Hook,
4+
JsonValue,
5+
OpenFeatureEventEmitter,
6+
Provider,
7+
ResolutionDetails,
8+
} from '@openfeature/react-sdk';
9+
import { evaluatePricing } from "../index";
10+
import { ExtendedFeatureStatus } from 'pricing4ts';
11+
12+
export class ReactPricingDrivenFeaturesProvider implements Provider {
13+
readonly metadata = {
14+
name: 'pricing-driven-features',
15+
description: 'An Open Feature provider that enables features based on pricing information',
16+
};
17+
18+
readonly runsOn = 'client';
19+
20+
events = new OpenFeatureEventEmitter();
21+
hooks?: Hook[] | undefined;
22+
status?: ClientProviderStatus | undefined;
23+
24+
private pricingUrl: string | undefined;
25+
private pricingYaml: string | undefined;
26+
private evaluation: Record<string, ExtendedFeatureStatus> = {};
27+
28+
onContextChange(oldContext: any, newContext: any): Promise<void> | void {
29+
this.evaluation = evaluatePricing(
30+
this.pricingYaml!,
31+
newContext.subscription,
32+
newContext.userContext!
33+
);
34+
}
35+
36+
resolveBooleanEvaluation(flagKey: string, defaultValue: boolean): ResolutionDetails<boolean> {
37+
try {
38+
return {
39+
value: this._evaluateFeature(flagKey).value.eval as boolean,
40+
};
41+
} catch (error) {
42+
console.error('Error occurred during evaluation. ERROR: ', (error as Error).message);
43+
return {
44+
value: defaultValue,
45+
};
46+
}
47+
}
48+
49+
resolveStringEvaluation(flagKey: string, defaultValue: string): ResolutionDetails<string> {
50+
try {
51+
const result = this._evaluateFeature(flagKey);
52+
return {
53+
value: result.value.eval.toString(),
54+
};
55+
} catch (error) {
56+
console.error('Error occurred during evaluation. ERROR: ', (error as Error).message);
57+
return {
58+
value: defaultValue,
59+
};
60+
}
61+
}
62+
63+
resolveNumberEvaluation(flagKey: string, defaultValue: number): ResolutionDetails<number> {
64+
try {
65+
const result = this._evaluateFeature(flagKey);
66+
return {
67+
value: result.value.eval ? 1 : 0,
68+
};
69+
} catch (error) {
70+
console.error('Error occurred during evaluation. ERROR: ', (error as Error).message);
71+
return {
72+
value: defaultValue,
73+
};
74+
}
75+
}
76+
77+
resolveObjectEvaluation<T extends JsonValue>(
78+
flagKey: string,
79+
defaultValue: T
80+
): ResolutionDetails<T> {
81+
try {
82+
return this._evaluateFeature(flagKey) as unknown as ResolutionDetails<T>;
83+
} catch (error) {
84+
console.error('Error occurred during evaluation. ERROR: ', (error as Error).message);
85+
return {
86+
value: defaultValue,
87+
} as ResolutionDetails<T>;
88+
}
89+
}
90+
91+
initialize?(context?: any): Promise<void> {
92+
if (context.pricingUrl) {
93+
this.pricingUrl = context.pricingUrl;
94+
if (!context.subscription) {
95+
return Promise.reject(
96+
"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."
97+
);
98+
}
99+
if (!context.userContext) {
100+
return Promise.reject(
101+
"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."
102+
);
103+
}
104+
return fetch(this.pricingUrl!)
105+
.then(response => response.text())
106+
.then(yaml => {
107+
this.pricingYaml = yaml;
108+
this.evaluation = evaluatePricing(
109+
this.pricingYaml,
110+
context.subscription,
111+
context.userContext!
112+
);
113+
console.log("PricingDrivenFeaturesProvider initialized with context: ", context);
114+
});
115+
} else {
116+
return Promise.reject(
117+
"Pricing URL not provided in context. Use 'pricingUrl' to provide one."
118+
);
119+
}
120+
}
121+
122+
private _evaluateFeature(flagKey: string): ResolutionDetails<any> {
123+
if (Object.keys(this.evaluation).length === 0) {
124+
return {
125+
value: {
126+
eval: false,
127+
used: null,
128+
limit: null,
129+
error: {
130+
code: 'PROVIDER_NOT_READY',
131+
message: 'Pricing not yet loaded into the provider',
132+
},
133+
},
134+
};
135+
} else {
136+
return {
137+
value: this.evaluation[flagKey],
138+
};
139+
}
140+
}
141+
}
142+

0 commit comments

Comments
 (0)