@frak-labs/core-sdk 1.1.4 → 1.1.5-beta.1700e6ec
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cdn/bundle.js +1 -1
- package/dist/actions-Cyn4asew.cjs +2 -0
- package/dist/actions-Cyn4asew.cjs.map +1 -0
- package/dist/actions-DtPVuk2e.js +2 -0
- package/dist/actions-DtPVuk2e.js.map +1 -0
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +3 -3
- package/dist/actions.d.ts +3 -3
- package/dist/actions.js +1 -1
- package/dist/bundle.cjs +1 -1
- package/dist/bundle.d.cts +5 -4
- package/dist/bundle.d.ts +5 -4
- package/dist/bundle.js +1 -1
- package/dist/frakContext-Bgidv-Se.js +2 -0
- package/dist/frakContext-Bgidv-Se.js.map +1 -0
- package/dist/frakContext-CeVlWFcX.cjs +2 -0
- package/dist/frakContext-CeVlWFcX.cjs.map +1 -0
- package/dist/getCurrencyAmountKey-DQcwjJiB.js +2 -0
- package/dist/getCurrencyAmountKey-DQcwjJiB.js.map +1 -0
- package/dist/getCurrencyAmountKey-h2eOiYzv.cjs +2 -0
- package/dist/getCurrencyAmountKey-h2eOiYzv.cjs.map +1 -0
- package/dist/{index-Cm1PNC9z.d.ts → index-CD-GW6rL.d.ts} +8 -6
- package/dist/{index-Cumn_FNI.d.cts → index-D5_EwZ3f.d.cts} +8 -6
- package/dist/{index-lcImYQyL.d.ts → index-DPez7SXh.d.cts} +4 -2
- package/dist/{index-DR4n-QOe.d.cts → index-DQYipoH_.d.ts} +4 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/merchantInformation--tLnowAN.d.ts +344 -0
- package/dist/merchantInformation-BSuahpjl.d.cts +344 -0
- package/dist/{openSso-BErs7_Bg.d.cts → openSso-CjZ8lAEA.d.ts} +8 -281
- package/dist/{openSso-DRLH4D9a.d.ts → openSso-DSXbUP-j.d.cts} +8 -281
- package/dist/rewards.cjs +2 -0
- package/dist/rewards.cjs.map +1 -0
- package/dist/rewards.d.cts +130 -0
- package/dist/rewards.d.ts +130 -0
- package/dist/rewards.js +2 -0
- package/dist/rewards.js.map +1 -0
- package/dist/src-BXtnEE3a.js +2 -0
- package/dist/src-BXtnEE3a.js.map +1 -0
- package/dist/src-TpX-7MwJ.cjs +2 -0
- package/dist/src-TpX-7MwJ.cjs.map +1 -0
- package/package.json +16 -3
- package/src/actions/getMerchantInformation.test.ts +2 -0
- package/src/actions/referral/processReferral.test.ts +4 -4
- package/src/actions/referral/processReferral.ts +4 -4
- package/src/clients/createIFrameFrakClient.ts +7 -7
- package/src/clients/transports/iframeLifecycleManager.ts +5 -21
- package/src/config/sdkConfigStore.ts +6 -3
- package/src/index.ts +6 -0
- package/src/rewards/conditions.test.ts +60 -0
- package/src/rewards/conditions.ts +72 -0
- package/src/rewards/example.test.ts +92 -0
- package/src/rewards/example.ts +57 -0
- package/src/rewards/format.test.ts +143 -0
- package/src/rewards/format.ts +81 -0
- package/src/rewards/index.ts +29 -0
- package/src/rewards/select.test.ts +281 -0
- package/src/rewards/select.ts +197 -0
- package/src/rewards/value.test.ts +89 -0
- package/src/rewards/value.ts +73 -0
- package/src/types/index.ts +6 -0
- package/src/types/resolvedConfig.ts +4 -0
- package/src/types/rpc/merchantInformation.ts +120 -13
- package/src/utils/analytics/events/lifecycle.ts +1 -1
- package/src/utils/analytics/events/referral.ts +3 -3
- package/src/utils/browser/deepLinkWithFallback.ts +4 -1
- package/src/utils/browser/inAppBrowser.ts +2 -2
- package/src/utils/cache/withCache.ts +30 -11
- package/src/utils/iframe/iframeHelper.test.ts +39 -0
- package/src/utils/iframe/iframeHelper.ts +31 -2
- package/dist/actions-D5GbNcvt.cjs +0 -1
- package/dist/actions-DE2dHqtp.js +0 -1
- package/dist/frakContext-Bl17GGa3.cjs +0 -1
- package/dist/frakContext-DmI5CaSw.js +0 -1
- package/dist/src-5vpUuhfY.js +0 -1
- package/dist/src-BQsjMpvA.cjs +0 -1
|
@@ -89,23 +89,26 @@ function removeCache(): void {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// ---------------------------------------------------------------------------
|
|
92
|
-
// Initialise window-backed config (
|
|
92
|
+
// Initialise window-backed config (lazily, on first access)
|
|
93
93
|
// ---------------------------------------------------------------------------
|
|
94
94
|
|
|
95
|
+
// Seed `window.__frakSdkConfig` from the localStorage cache. Do NOT call this at
|
|
96
|
+
// module top-level: that side effect contradicts `sideEffects: false` (a bundler
|
|
97
|
+
// may legally drop it) and forces a localStorage read on every consumer at import.
|
|
98
|
+
// It is invoked lazily from `getConfig` instead.
|
|
95
99
|
function initConfig(): void {
|
|
96
100
|
if (!isBrowser) return;
|
|
97
101
|
if (window[GLOBAL_KEY]) return;
|
|
98
102
|
window[GLOBAL_KEY] = readCache() ?? freshEmptyConfig();
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
initConfig();
|
|
102
|
-
|
|
103
105
|
// ---------------------------------------------------------------------------
|
|
104
106
|
// Helpers
|
|
105
107
|
// ---------------------------------------------------------------------------
|
|
106
108
|
|
|
107
109
|
function getConfig(): SdkResolvedConfig {
|
|
108
110
|
if (!isBrowser) return freshEmptyConfig();
|
|
111
|
+
initConfig();
|
|
109
112
|
return window[GLOBAL_KEY] ?? freshEmptyConfig();
|
|
110
113
|
}
|
|
111
114
|
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type {
|
|
|
23
23
|
AttributionParams,
|
|
24
24
|
ClientLifecycleEvent,
|
|
25
25
|
CompressedData,
|
|
26
|
+
ConditionGroup,
|
|
27
|
+
ConditionOperator,
|
|
26
28
|
Currency,
|
|
27
29
|
// RPC Embedded wallet
|
|
28
30
|
DisplayEmbeddedWalletParamsType,
|
|
@@ -61,6 +63,7 @@ export type {
|
|
|
61
63
|
LoggedOutEmbeddedView,
|
|
62
64
|
LoginModalStepType,
|
|
63
65
|
MerchantConfigResponse,
|
|
66
|
+
MerchantReward,
|
|
64
67
|
ModalRpcMetadata,
|
|
65
68
|
ModalRpcStepsInput,
|
|
66
69
|
ModalRpcStepsResultType,
|
|
@@ -75,6 +78,9 @@ export type {
|
|
|
75
78
|
ResolvedPlacement,
|
|
76
79
|
ResolvedSdkConfig,
|
|
77
80
|
RewardTier,
|
|
81
|
+
RuleCondition,
|
|
82
|
+
RuleConditions,
|
|
83
|
+
RuleField,
|
|
78
84
|
SdkResolvedConfig,
|
|
79
85
|
// RPC Interaction
|
|
80
86
|
SendInteractionParamsType,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { extractMinPurchaseAmount, extractStartDate } from "./conditions";
|
|
3
|
+
|
|
4
|
+
const unix = (iso: string) => Math.floor(new Date(iso).getTime() / 1000);
|
|
5
|
+
|
|
6
|
+
describe("extractMinPurchaseAmount", () => {
|
|
7
|
+
it("reads a flat purchase.amount gte condition", () => {
|
|
8
|
+
expect(
|
|
9
|
+
extractMinPurchaseAmount([
|
|
10
|
+
{ field: "purchase.amount", operator: "gte", value: 50 },
|
|
11
|
+
])
|
|
12
|
+
).toBe(50);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("walks nested groups and keeps the lowest threshold", () => {
|
|
16
|
+
expect(
|
|
17
|
+
extractMinPurchaseAmount({
|
|
18
|
+
logic: "all",
|
|
19
|
+
conditions: [
|
|
20
|
+
{ field: "purchase.amount", operator: "gte", value: 80 },
|
|
21
|
+
{
|
|
22
|
+
logic: "any",
|
|
23
|
+
conditions: [
|
|
24
|
+
{
|
|
25
|
+
field: "purchase.amount",
|
|
26
|
+
operator: "gt",
|
|
27
|
+
value: 30,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
})
|
|
33
|
+
).toBe(30);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns undefined without a purchase gate", () => {
|
|
37
|
+
expect(
|
|
38
|
+
extractMinPurchaseAmount([
|
|
39
|
+
{ field: "user.isNew", operator: "eq", value: true },
|
|
40
|
+
])
|
|
41
|
+
).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("extractStartDate", () => {
|
|
46
|
+
it("reads a time.timestamp gte condition (unix seconds)", () => {
|
|
47
|
+
const start = extractStartDate([
|
|
48
|
+
{
|
|
49
|
+
field: "time.timestamp",
|
|
50
|
+
operator: "gte",
|
|
51
|
+
value: unix("2025-02-01T00:00:00Z"),
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
expect(start?.toISOString()).toBe("2025-02-01T00:00:00.000Z");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns undefined without a start gate", () => {
|
|
58
|
+
expect(extractStartDate([])).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ConditionGroup, RuleCondition, RuleConditions } from "../types";
|
|
2
|
+
|
|
3
|
+
type ConditionNode = RuleCondition | ConditionGroup;
|
|
4
|
+
|
|
5
|
+
function conditionNodes(conditions: RuleConditions): ConditionNode[] {
|
|
6
|
+
return Array.isArray(conditions) ? conditions : conditions.conditions;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function conditionValueToNumber(
|
|
10
|
+
value: RuleCondition["value"]
|
|
11
|
+
): number | undefined {
|
|
12
|
+
if (typeof value === "number") return value;
|
|
13
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Conditions arrive as a flat list or recursive groups; collect every numeric
|
|
21
|
+
// value whose field + operator match, descending into nested groups.
|
|
22
|
+
function collectConditionValues(
|
|
23
|
+
conditions: RuleConditions,
|
|
24
|
+
field: string,
|
|
25
|
+
operators: ReadonlySet<RuleCondition["operator"]>
|
|
26
|
+
): number[] {
|
|
27
|
+
const values: number[] = [];
|
|
28
|
+
for (const node of conditionNodes(conditions)) {
|
|
29
|
+
if ("logic" in node) {
|
|
30
|
+
values.push(...collectConditionValues(node, field, operators));
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (node.field === field && operators.has(node.operator)) {
|
|
34
|
+
const value = conditionValueToNumber(node.value);
|
|
35
|
+
if (value != null) values.push(value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return values;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const MIN_PURCHASE_OPERATORS: ReadonlySet<RuleCondition["operator"]> = new Set([
|
|
42
|
+
"gt",
|
|
43
|
+
"gte",
|
|
44
|
+
"between",
|
|
45
|
+
]);
|
|
46
|
+
const START_DATE_OPERATORS: ReadonlySet<RuleCondition["operator"]> = new Set([
|
|
47
|
+
"gt",
|
|
48
|
+
"gte",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
export function extractMinPurchaseAmount(
|
|
52
|
+
conditions: RuleConditions
|
|
53
|
+
): number | undefined {
|
|
54
|
+
const values = collectConditionValues(
|
|
55
|
+
conditions,
|
|
56
|
+
"purchase.amount",
|
|
57
|
+
MIN_PURCHASE_OPERATORS
|
|
58
|
+
);
|
|
59
|
+
return values.length > 0 ? Math.min(...values) : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The business app encodes a campaign start as a `time.timestamp >= <unix s>`
|
|
63
|
+
// rule condition; the endpoint returns it verbatim inside `conditions`.
|
|
64
|
+
export function extractStartDate(conditions: RuleConditions): Date | undefined {
|
|
65
|
+
const values = collectConditionValues(
|
|
66
|
+
conditions,
|
|
67
|
+
"time.timestamp",
|
|
68
|
+
START_DATE_OPERATORS
|
|
69
|
+
);
|
|
70
|
+
if (values.length === 0) return undefined;
|
|
71
|
+
return new Date(Math.min(...values) * 1000);
|
|
72
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { EstimatedReward } from "../types";
|
|
3
|
+
import {
|
|
4
|
+
buildPercentageExample,
|
|
5
|
+
buildTierExample,
|
|
6
|
+
pickFlatBasket,
|
|
7
|
+
pickTierBasket,
|
|
8
|
+
} from "./example";
|
|
9
|
+
|
|
10
|
+
type PercentageReward = Extract<EstimatedReward, { payoutType: "percentage" }>;
|
|
11
|
+
|
|
12
|
+
function percentage(
|
|
13
|
+
percent: number,
|
|
14
|
+
bounds?: { min?: number; max?: number }
|
|
15
|
+
): PercentageReward {
|
|
16
|
+
const toAmount = (eur: number) => ({
|
|
17
|
+
amount: eur,
|
|
18
|
+
eurAmount: eur,
|
|
19
|
+
usdAmount: eur,
|
|
20
|
+
gbpAmount: eur,
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
payoutType: "percentage",
|
|
24
|
+
percent,
|
|
25
|
+
percentOf: "purchase_amount",
|
|
26
|
+
minAmount: bounds?.min != null ? toAmount(bounds.min) : undefined,
|
|
27
|
+
maxAmount: bounds?.max != null ? toAmount(bounds.max) : undefined,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("pickFlatBasket", () => {
|
|
32
|
+
it("defaults to the reference basket of 100", () => {
|
|
33
|
+
expect(pickFlatBasket(undefined)).toBe(100);
|
|
34
|
+
expect(pickFlatBasket(40)).toBe(100);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("uses the minimum purchase when it exceeds the reference", () => {
|
|
38
|
+
expect(pickFlatBasket(150)).toBe(150);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("pickTierBasket", () => {
|
|
43
|
+
it("uses the reference when it fits the tier range", () => {
|
|
44
|
+
expect(pickTierBasket(80, 200)).toBe(100);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("uses the lower bound for an open-ended tier", () => {
|
|
48
|
+
expect(pickTierBasket(80, undefined)).toBe(80);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("picks a round number inside a bounded tier excluding the reference", () => {
|
|
52
|
+
expect(pickTierBasket(0, 30)).toBe(20);
|
|
53
|
+
expect(pickTierBasket(30, 80)).toBe(60);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("buildPercentageExample", () => {
|
|
58
|
+
it("computes percent of the basket", () => {
|
|
59
|
+
const example = buildPercentageExample(percentage(8), undefined);
|
|
60
|
+
expect(example?.basket).toBe(100);
|
|
61
|
+
expect(example?.reward).toBeCloseTo(8);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("clamps the reward to the configured max amount", () => {
|
|
65
|
+
const example = buildPercentageExample(
|
|
66
|
+
percentage(8, { max: 4.8 }),
|
|
67
|
+
undefined
|
|
68
|
+
);
|
|
69
|
+
expect(example?.basket).toBe(100);
|
|
70
|
+
expect(example?.reward).toBeCloseTo(4.8);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("uses the minimum purchase as the basket when higher", () => {
|
|
74
|
+
const example = buildPercentageExample(percentage(5), 200);
|
|
75
|
+
expect(example?.basket).toBe(200);
|
|
76
|
+
expect(example?.reward).toBeCloseTo(10);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("buildTierExample", () => {
|
|
81
|
+
it("computes percent of the open tier lower bound", () => {
|
|
82
|
+
const example = buildTierExample(12, 80, undefined);
|
|
83
|
+
expect(example?.basket).toBe(80);
|
|
84
|
+
expect(example?.reward).toBeCloseTo(9.6);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("computes percent of a round basket inside a bounded tier", () => {
|
|
88
|
+
const example = buildTierExample(5, 30, 80);
|
|
89
|
+
expect(example?.basket).toBe(60);
|
|
90
|
+
expect(example?.reward).toBeCloseTo(3);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { EstimatedReward } from "../types";
|
|
2
|
+
|
|
3
|
+
type PercentageReward = Extract<EstimatedReward, { payoutType: "percentage" }>;
|
|
4
|
+
|
|
5
|
+
// Reference order total used to turn a "% of basket" reward into a concrete
|
|
6
|
+
// example amount. The campaign's minimum purchase overrides it when higher.
|
|
7
|
+
const REFERENCE_BASKET = 100;
|
|
8
|
+
|
|
9
|
+
export type RewardExample = {
|
|
10
|
+
basket: number;
|
|
11
|
+
reward: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function roundWithinRange(min: number, max: number): number {
|
|
15
|
+
const rounded = Math.round((min + max) / 2 / 10) * 10;
|
|
16
|
+
return Math.min(max, Math.max(min, rounded));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function pickFlatBasket(minPurchase: number | undefined): number {
|
|
20
|
+
return Math.max(REFERENCE_BASKET, minPurchase ?? 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Basket for a percentage tier: the reference when it sits inside the tier
|
|
24
|
+
// range, a round number within the range otherwise, or the lower bound for an
|
|
25
|
+
// open-ended (uncapped) top tier.
|
|
26
|
+
export function pickTierBasket(
|
|
27
|
+
minValue: number,
|
|
28
|
+
maxValue: number | undefined
|
|
29
|
+
): number {
|
|
30
|
+
if (maxValue == null) return minValue;
|
|
31
|
+
if (minValue <= REFERENCE_BASKET && REFERENCE_BASKET <= maxValue) {
|
|
32
|
+
return REFERENCE_BASKET;
|
|
33
|
+
}
|
|
34
|
+
return roundWithinRange(minValue, maxValue);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildPercentageExample(
|
|
38
|
+
reward: PercentageReward,
|
|
39
|
+
minPurchase: number | undefined
|
|
40
|
+
): RewardExample | undefined {
|
|
41
|
+
const basket = pickFlatBasket(minPurchase);
|
|
42
|
+
if (basket <= 0) return undefined;
|
|
43
|
+
let amount = (reward.percent / 100) * basket;
|
|
44
|
+
if (reward.minAmount) amount = Math.max(amount, reward.minAmount.eurAmount);
|
|
45
|
+
if (reward.maxAmount) amount = Math.min(amount, reward.maxAmount.eurAmount);
|
|
46
|
+
return { basket, reward: amount };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildTierExample(
|
|
50
|
+
percent: number,
|
|
51
|
+
minValue: number,
|
|
52
|
+
maxValue: number | undefined
|
|
53
|
+
): RewardExample | undefined {
|
|
54
|
+
const basket = pickTierBasket(minValue, maxValue);
|
|
55
|
+
if (basket <= 0) return undefined;
|
|
56
|
+
return { basket, reward: (percent / 100) * basket };
|
|
57
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { EstimatedReward } from "../types";
|
|
3
|
+
import {
|
|
4
|
+
applyRewardPlaceholder,
|
|
5
|
+
formatEstimatedReward,
|
|
6
|
+
formatRewardOrHide,
|
|
7
|
+
} from "./format";
|
|
8
|
+
|
|
9
|
+
const amount = (eur: number) => ({
|
|
10
|
+
amount: eur,
|
|
11
|
+
eurAmount: eur,
|
|
12
|
+
usdAmount: eur,
|
|
13
|
+
gbpAmount: eur,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("formatEstimatedReward", () => {
|
|
17
|
+
it("formats a fixed reward in the default currency, rounded", () => {
|
|
18
|
+
const reward: EstimatedReward = {
|
|
19
|
+
payoutType: "fixed",
|
|
20
|
+
amount: amount(5.4),
|
|
21
|
+
};
|
|
22
|
+
// Intl renders a narrow no-break space before "€" — assert content.
|
|
23
|
+
const formatted = formatEstimatedReward(reward);
|
|
24
|
+
expect(formatted).toContain("5");
|
|
25
|
+
expect(formatted).not.toContain("6");
|
|
26
|
+
expect(formatted).toContain("€");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("renders a percentage reward as a percent string (never a basket amount)", () => {
|
|
30
|
+
const reward: EstimatedReward = {
|
|
31
|
+
payoutType: "percentage",
|
|
32
|
+
percent: 10,
|
|
33
|
+
percentOf: "purchase_amount",
|
|
34
|
+
maxAmount: amount(50),
|
|
35
|
+
};
|
|
36
|
+
expect(formatEstimatedReward(reward)).toBe("10 %");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("uses the richest token tier for a tiered reward", () => {
|
|
40
|
+
const reward: EstimatedReward = {
|
|
41
|
+
payoutType: "tiered",
|
|
42
|
+
tierField: "purchase.amount",
|
|
43
|
+
tiers: [
|
|
44
|
+
{ minValue: 0, maxValue: 50, amount: amount(2) },
|
|
45
|
+
{ minValue: 50, amount: amount(8) },
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
const formatted = formatEstimatedReward(reward);
|
|
49
|
+
expect(formatted).toContain("8");
|
|
50
|
+
expect(formatted).toContain("€");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("falls back to the max percent when tiers carry no token amount", () => {
|
|
54
|
+
const reward: EstimatedReward = {
|
|
55
|
+
payoutType: "tiered",
|
|
56
|
+
tierField: "purchase.amount",
|
|
57
|
+
tiers: [
|
|
58
|
+
{ minValue: 0, maxValue: 50, percent: 5 },
|
|
59
|
+
{ minValue: 50, percent: 12 },
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
expect(formatEstimatedReward(reward)).toBe("12 %");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("respects the requested currency", () => {
|
|
66
|
+
const reward: EstimatedReward = {
|
|
67
|
+
payoutType: "fixed",
|
|
68
|
+
amount: amount(5),
|
|
69
|
+
};
|
|
70
|
+
const formatted = formatEstimatedReward(reward, "usd");
|
|
71
|
+
expect(formatted).toContain("5");
|
|
72
|
+
expect(formatted).toContain("$");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("formatRewardOrHide", () => {
|
|
77
|
+
it("returns undefined for a missing reward", () => {
|
|
78
|
+
expect(formatRewardOrHide(undefined)).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("formats a fixed reward that carries money value", () => {
|
|
82
|
+
const formatted = formatRewardOrHide({
|
|
83
|
+
payoutType: "fixed",
|
|
84
|
+
amount: amount(5),
|
|
85
|
+
});
|
|
86
|
+
expect(formatted).toContain("5");
|
|
87
|
+
expect(formatted).toContain("€");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("hides a fixed reward with no money value", () => {
|
|
91
|
+
expect(
|
|
92
|
+
formatRewardOrHide({ payoutType: "fixed", amount: amount(0) })
|
|
93
|
+
).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("always shows a capped percentage as a percent string", () => {
|
|
97
|
+
expect(
|
|
98
|
+
formatRewardOrHide({
|
|
99
|
+
payoutType: "percentage",
|
|
100
|
+
percent: 10,
|
|
101
|
+
percentOf: "purchase_amount",
|
|
102
|
+
maxAmount: amount(50),
|
|
103
|
+
})
|
|
104
|
+
).toBe("10 %");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("shows an uncapped percentage as a percent string (no money value)", () => {
|
|
108
|
+
expect(
|
|
109
|
+
formatRewardOrHide({
|
|
110
|
+
payoutType: "percentage",
|
|
111
|
+
percent: 8,
|
|
112
|
+
percentOf: "purchase_amount",
|
|
113
|
+
})
|
|
114
|
+
).toBe("8 %");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("shows a percent-only tiered reward instead of hiding it", () => {
|
|
118
|
+
expect(
|
|
119
|
+
formatRewardOrHide({
|
|
120
|
+
payoutType: "tiered",
|
|
121
|
+
tierField: "purchase.amount",
|
|
122
|
+
tiers: [
|
|
123
|
+
{ minValue: 0, maxValue: 50, percent: 5 },
|
|
124
|
+
{ minValue: 50, percent: 12 },
|
|
125
|
+
],
|
|
126
|
+
})
|
|
127
|
+
).toBe("12 %");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("applyRewardPlaceholder", () => {
|
|
132
|
+
it("substitutes the reward into the placeholder", () => {
|
|
133
|
+
expect(applyRewardPlaceholder("Earn {REWARD} now", "5 €")).toBe(
|
|
134
|
+
"Earn 5 € now"
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("strips the placeholder when no reward is provided", () => {
|
|
139
|
+
expect(applyRewardPlaceholder("Earn {REWARD} now", undefined)).toBe(
|
|
140
|
+
"Earn now"
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Currency, EstimatedReward } from "../types";
|
|
2
|
+
import { formatAmount } from "../utils/format/formatAmount";
|
|
3
|
+
import { getCurrencyAmountKey } from "../utils/format/getCurrencyAmountKey";
|
|
4
|
+
import { getSupportedCurrency } from "../utils/format/getSupportedCurrency";
|
|
5
|
+
import { getRewardRank, getRewardValue, maxRewardPercent } from "./value";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format an {@link EstimatedReward} into a human-readable string.
|
|
9
|
+
*
|
|
10
|
+
* - `fixed` → e.g. `"5 €"`
|
|
11
|
+
* - `percentage` → e.g. `"10 %"`
|
|
12
|
+
* - `tiered` → the richest tier: max token amount (e.g. `"50 €"`) or, when
|
|
13
|
+
* tiers only carry percentages, the max percent (e.g. `"10 %"`)
|
|
14
|
+
*
|
|
15
|
+
* Percentages are always rendered as a `"X %"` string: the backend never sends
|
|
16
|
+
* a reference basket, so the reward cannot be resolved to a concrete amount
|
|
17
|
+
* here. Callers that need a worked example use the `example` helpers instead.
|
|
18
|
+
*/
|
|
19
|
+
export function formatEstimatedReward(
|
|
20
|
+
reward: EstimatedReward,
|
|
21
|
+
currency?: Currency
|
|
22
|
+
): string {
|
|
23
|
+
const supportedCurrency = getSupportedCurrency(currency);
|
|
24
|
+
const key = getCurrencyAmountKey(supportedCurrency);
|
|
25
|
+
|
|
26
|
+
switch (reward.payoutType) {
|
|
27
|
+
case "fixed":
|
|
28
|
+
return formatAmount(
|
|
29
|
+
Math.round(reward.amount[key]),
|
|
30
|
+
supportedCurrency
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
case "percentage":
|
|
34
|
+
return `${reward.percent} %`;
|
|
35
|
+
|
|
36
|
+
case "tiered": {
|
|
37
|
+
const maxAmount = getRewardValue(reward, key);
|
|
38
|
+
if (maxAmount > 0) {
|
|
39
|
+
return formatAmount(Math.round(maxAmount), supportedCurrency);
|
|
40
|
+
}
|
|
41
|
+
const maxPercent = maxRewardPercent(reward);
|
|
42
|
+
if (maxPercent > 0) {
|
|
43
|
+
return `${maxPercent} %`;
|
|
44
|
+
}
|
|
45
|
+
return formatAmount(0, supportedCurrency);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format a reward for display, or return `undefined` when it is not worth
|
|
52
|
+
* advertising. Callers rely on `undefined` to hide a badge / fall back to
|
|
53
|
+
* other copy.
|
|
54
|
+
*
|
|
55
|
+
* A reward is hidden only when it carries no displayable value — a `fixed` or
|
|
56
|
+
* `tiered` reward whose money value is `0` (e.g. an unknown token price). An
|
|
57
|
+
* uncapped percentage always renders as `"X %"`, since the percent itself is
|
|
58
|
+
* meaningful even without a money value.
|
|
59
|
+
*/
|
|
60
|
+
export function formatRewardOrHide(
|
|
61
|
+
reward: EstimatedReward | undefined,
|
|
62
|
+
currency?: Currency
|
|
63
|
+
): string | undefined {
|
|
64
|
+
if (!reward) return undefined;
|
|
65
|
+
const key = getCurrencyAmountKey(getSupportedCurrency(currency));
|
|
66
|
+
if (getRewardRank(reward, key) <= 0) return undefined;
|
|
67
|
+
return formatEstimatedReward(reward, currency);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Replace the `{REWARD}` placeholder in a text string with a reward value.
|
|
72
|
+
* If no reward is provided, returns the text with `{REWARD}` stripped.
|
|
73
|
+
*/
|
|
74
|
+
export function applyRewardPlaceholder(
|
|
75
|
+
text: string,
|
|
76
|
+
reward: string | undefined
|
|
77
|
+
): string {
|
|
78
|
+
return reward
|
|
79
|
+
? text.replace("{REWARD}", reward)
|
|
80
|
+
: text.replace("{REWARD}", "");
|
|
81
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Framework-agnostic reward display logic shared across the SDK, listener,
|
|
2
|
+
// wallet and wallet-shared. Depends only on `@frak-labs/core-sdk`'s own reward
|
|
3
|
+
// types (`EstimatedReward`, `MerchantReward`, `RuleConditions`) — no backend or
|
|
4
|
+
// framework coupling — so it stays safe to ship inside the published SDK and
|
|
5
|
+
// lets core-sdk consume its own reward logic without a dependency cycle.
|
|
6
|
+
|
|
7
|
+
export { extractMinPurchaseAmount, extractStartDate } from "./conditions";
|
|
8
|
+
export {
|
|
9
|
+
buildPercentageExample,
|
|
10
|
+
buildTierExample,
|
|
11
|
+
pickFlatBasket,
|
|
12
|
+
pickTierBasket,
|
|
13
|
+
type RewardExample,
|
|
14
|
+
} from "./example";
|
|
15
|
+
export {
|
|
16
|
+
applyRewardPlaceholder,
|
|
17
|
+
formatEstimatedReward,
|
|
18
|
+
formatRewardOrHide,
|
|
19
|
+
} from "./format";
|
|
20
|
+
export {
|
|
21
|
+
type BestReward,
|
|
22
|
+
type DisplayCampaign,
|
|
23
|
+
formatBestReward,
|
|
24
|
+
type RewardAudience,
|
|
25
|
+
type SelectDisplayCampaignOptions,
|
|
26
|
+
selectBestReward,
|
|
27
|
+
selectDisplayCampaign,
|
|
28
|
+
} from "./select";
|
|
29
|
+
export { getRewardValue } from "./value";
|