@frak-labs/core-sdk 1.1.5 → 1.1.6
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-DrZ_PIs1.cjs → actions-Cyn4asew.cjs} +1 -1
- package/dist/{actions-DrZ_PIs1.cjs.map → actions-Cyn4asew.cjs.map} +1 -1
- package/dist/{actions-HG5M8wX7.js → actions-DtPVuk2e.js} +1 -1
- package/dist/{actions-HG5M8wX7.js.map → actions-DtPVuk2e.js.map} +1 -1
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +2 -2
- package/dist/actions.d.ts +2 -2
- 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/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-BysidNLB.d.ts → index-CD-GW6rL.d.ts} +3 -2
- package/dist/{index-k1lfbVkl.d.cts → index-D5_EwZ3f.d.cts} +3 -2
- package/dist/{index-C1A3I4x1.d.cts → index-DPez7SXh.d.cts} +3 -2
- package/dist/{index-BJMazhpm.d.ts → index-DQYipoH_.d.ts} +3 -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-BWeD0Fb2.d.ts → openSso-CjZ8lAEA.d.ts} +7 -286
- package/dist/{openSso-B0igO6Iu.d.cts → openSso-DSXbUP-j.d.cts} +7 -286
- 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-CTtM8g9p.cjs +2 -0
- package/dist/src-CTtM8g9p.cjs.map +1 -0
- package/dist/src-Dq2hi427.js +2 -0
- package/dist/src-Dq2hi427.js.map +1 -0
- package/package.json +13 -1
- package/src/actions/getMerchantInformation.test.ts +2 -0
- 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 +107 -7
- package/dist/src-DXE40XuK.js +0 -2
- package/dist/src-DXE40XuK.js.map +0 -1
- package/dist/src-hwLbVkQI.cjs +0 -2
- package/dist/src-hwLbVkQI.cjs.map +0 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { InteractionTypeKey } from "../constants/interactionTypes";
|
|
2
|
+
import type {
|
|
3
|
+
Currency,
|
|
4
|
+
EstimatedReward,
|
|
5
|
+
MerchantReward,
|
|
6
|
+
TokenAmountType,
|
|
7
|
+
} from "../types";
|
|
8
|
+
import { formatAmount } from "../utils/format/formatAmount";
|
|
9
|
+
import { getCurrencyAmountKey } from "../utils/format/getCurrencyAmountKey";
|
|
10
|
+
import { getSupportedCurrency } from "../utils/format/getSupportedCurrency";
|
|
11
|
+
import { extractMinPurchaseAmount, extractStartDate } from "./conditions";
|
|
12
|
+
import { formatRewardOrHide } from "./format";
|
|
13
|
+
import { getRewardRank } from "./value";
|
|
14
|
+
|
|
15
|
+
/** Reward side a surface cares about: the sharer (`referrer`) or the referred
|
|
16
|
+
* user (`referee`). Drives both campaign ranking and which side is displayed. */
|
|
17
|
+
export type RewardAudience = "referrer" | "referee";
|
|
18
|
+
|
|
19
|
+
export type DisplayCampaign = {
|
|
20
|
+
campaign: MerchantReward;
|
|
21
|
+
status: "live" | "upcoming";
|
|
22
|
+
startsAt?: Date;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type SelectDisplayCampaignOptions = {
|
|
26
|
+
/** Reference "now" for expiry / start gating; defaults to the current time. */
|
|
27
|
+
now?: Date;
|
|
28
|
+
/** Currency the ranking is expressed in; defaults to EUR. */
|
|
29
|
+
currency?: Currency;
|
|
30
|
+
/** When set, only campaigns triggered by this interaction are considered. */
|
|
31
|
+
targetInteraction?: InteractionTypeKey;
|
|
32
|
+
/** Reward side to rank campaigns by; defaults to `"referrer"`. */
|
|
33
|
+
audience?: RewardAudience;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function isExpired(campaign: MerchantReward, nowMs: number): boolean {
|
|
37
|
+
return (
|
|
38
|
+
campaign.expiresAt != null &&
|
|
39
|
+
new Date(campaign.expiresAt).getTime() <= nowMs
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasStarted(campaign: MerchantReward, nowMs: number): boolean {
|
|
44
|
+
const startsAt = extractStartDate(campaign.conditions);
|
|
45
|
+
return startsAt == null || startsAt.getTime() <= nowMs;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function audienceReward(campaign: MerchantReward, audience: RewardAudience) {
|
|
49
|
+
return audience === "referee" ? campaign.referee : campaign.referrer;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function campaignRank(
|
|
53
|
+
campaign: MerchantReward,
|
|
54
|
+
key: keyof TokenAmountType,
|
|
55
|
+
audience: RewardAudience
|
|
56
|
+
): number {
|
|
57
|
+
const reward = audienceReward(campaign, audience);
|
|
58
|
+
return reward ? getRewardRank(reward, key) : 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Pick the single campaign a merchant surface should display.
|
|
63
|
+
*
|
|
64
|
+
* Filters out expired (and, when `targetInteraction` is set, non-matching)
|
|
65
|
+
* campaigns, then prefers the highest-ranked *live* campaign — ranked by the
|
|
66
|
+
* `audience` reward side in the requested currency. When none has started yet
|
|
67
|
+
* it falls back to the soonest-starting upcoming campaign (the endpoint does
|
|
68
|
+
* not gate on the start-date condition, so future-start campaigns come
|
|
69
|
+
* through).
|
|
70
|
+
*/
|
|
71
|
+
export function selectDisplayCampaign(
|
|
72
|
+
rewards: readonly MerchantReward[],
|
|
73
|
+
options: SelectDisplayCampaignOptions = {}
|
|
74
|
+
): DisplayCampaign | undefined {
|
|
75
|
+
const nowMs = (options.now ?? new Date()).getTime();
|
|
76
|
+
const audience = options.audience ?? "referrer";
|
|
77
|
+
const key = getCurrencyAmountKey(getSupportedCurrency(options.currency));
|
|
78
|
+
|
|
79
|
+
const matching = options.targetInteraction
|
|
80
|
+
? rewards.filter(
|
|
81
|
+
(campaign) =>
|
|
82
|
+
campaign.interactionTypeKey === options.targetInteraction
|
|
83
|
+
)
|
|
84
|
+
: rewards;
|
|
85
|
+
const active = matching.filter((campaign) => !isExpired(campaign, nowMs));
|
|
86
|
+
|
|
87
|
+
const live = active.filter((campaign) => hasStarted(campaign, nowMs));
|
|
88
|
+
if (live.length > 0) {
|
|
89
|
+
const best = live.reduce((a, b) =>
|
|
90
|
+
campaignRank(b, key, audience) > campaignRank(a, key, audience)
|
|
91
|
+
? b
|
|
92
|
+
: a
|
|
93
|
+
);
|
|
94
|
+
return { campaign: best, status: "live" };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const upcoming = active
|
|
98
|
+
.map((campaign) => ({
|
|
99
|
+
campaign,
|
|
100
|
+
startsAt: extractStartDate(campaign.conditions),
|
|
101
|
+
}))
|
|
102
|
+
.filter(
|
|
103
|
+
(entry): entry is { campaign: MerchantReward; startsAt: Date } =>
|
|
104
|
+
entry.startsAt != null
|
|
105
|
+
);
|
|
106
|
+
if (upcoming.length === 0) return undefined;
|
|
107
|
+
|
|
108
|
+
const soonest = upcoming.reduce((a, b) =>
|
|
109
|
+
b.startsAt.getTime() < a.startsAt.getTime() ? b : a
|
|
110
|
+
);
|
|
111
|
+
return {
|
|
112
|
+
campaign: soonest.campaign,
|
|
113
|
+
status: "upcoming",
|
|
114
|
+
startsAt: soonest.startsAt,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The single reward a merchant surface should display: its formatted string
|
|
120
|
+
* plus the `payoutType` of the underlying reward, so surfaces can adapt their
|
|
121
|
+
* presentation (e.g. hide percentage rewards, prefix tiered ones with "Up to").
|
|
122
|
+
*/
|
|
123
|
+
export type BestReward = {
|
|
124
|
+
/** Display-ready reward string (e.g. `"5 €"`, `"10 %"`). */
|
|
125
|
+
formatted: string;
|
|
126
|
+
/** Payout type of the selected reward. */
|
|
127
|
+
payoutType: EstimatedReward["payoutType"];
|
|
128
|
+
/**
|
|
129
|
+
* Minimum purchase amount gating the reward, formatted with the requested
|
|
130
|
+
* currency (e.g. `"10 €"`), or `undefined` when the campaign sets no
|
|
131
|
+
* minimum.
|
|
132
|
+
*/
|
|
133
|
+
minPurchaseAmount?: string;
|
|
134
|
+
/**
|
|
135
|
+
* Whole-day lockup applied before the reward settles, or `undefined` when
|
|
136
|
+
* the campaign has no lockup.
|
|
137
|
+
*/
|
|
138
|
+
lockupDurationDays?: number;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Pick the best campaign for `options` and resolve its `audience`-side reward
|
|
143
|
+
* to a formatted string plus its `payoutType`, or `undefined` when there is
|
|
144
|
+
* nothing worth showing.
|
|
145
|
+
*
|
|
146
|
+
* Single entry point shared by every "headline reward" surface (share button,
|
|
147
|
+
* wallet modal, sharing/install screens) so they all show the same number for
|
|
148
|
+
* a given merchant and can branch on the payout type.
|
|
149
|
+
*/
|
|
150
|
+
export function selectBestReward(
|
|
151
|
+
rewards: readonly MerchantReward[],
|
|
152
|
+
options: SelectDisplayCampaignOptions = {}
|
|
153
|
+
): BestReward | undefined {
|
|
154
|
+
const selected = selectDisplayCampaign(rewards, options);
|
|
155
|
+
if (!selected) return undefined;
|
|
156
|
+
const reward = audienceReward(
|
|
157
|
+
selected.campaign,
|
|
158
|
+
options.audience ?? "referrer"
|
|
159
|
+
);
|
|
160
|
+
if (!reward) return undefined;
|
|
161
|
+
const formatted = formatRewardOrHide(reward, options.currency);
|
|
162
|
+
if (!formatted) return undefined;
|
|
163
|
+
|
|
164
|
+
const minPurchase = extractMinPurchaseAmount(selected.campaign.conditions);
|
|
165
|
+
const minPurchaseAmount =
|
|
166
|
+
minPurchase != null
|
|
167
|
+
? formatAmount(minPurchase, options.currency)
|
|
168
|
+
: undefined;
|
|
169
|
+
|
|
170
|
+
const lockupSeconds = selected.campaign.defaultLockupSeconds;
|
|
171
|
+
const lockupDurationDays =
|
|
172
|
+
lockupSeconds && lockupSeconds > 0
|
|
173
|
+
? Math.round(lockupSeconds / 86_400)
|
|
174
|
+
: undefined;
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
formatted,
|
|
178
|
+
payoutType: reward.payoutType,
|
|
179
|
+
minPurchaseAmount,
|
|
180
|
+
lockupDurationDays,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Headline reward string for a merchant: picks the best campaign for `options`
|
|
186
|
+
* and formats its `audience`-side reward, or returns `undefined` when there is
|
|
187
|
+
* nothing worth showing.
|
|
188
|
+
*
|
|
189
|
+
* Thin wrapper over {@link selectBestReward} for callers that only need the
|
|
190
|
+
* formatted string.
|
|
191
|
+
*/
|
|
192
|
+
export function formatBestReward(
|
|
193
|
+
rewards: readonly MerchantReward[],
|
|
194
|
+
options: SelectDisplayCampaignOptions = {}
|
|
195
|
+
): string | undefined {
|
|
196
|
+
return selectBestReward(rewards, options)?.formatted;
|
|
197
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { EstimatedReward } from "../types";
|
|
3
|
+
import { getRewardRank, getRewardValue } from "./value";
|
|
4
|
+
|
|
5
|
+
const amount = (eur: number) => ({
|
|
6
|
+
amount: eur,
|
|
7
|
+
eurAmount: eur,
|
|
8
|
+
usdAmount: eur,
|
|
9
|
+
gbpAmount: eur,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const fixed = (eur: number): EstimatedReward => ({
|
|
13
|
+
payoutType: "fixed",
|
|
14
|
+
amount: amount(eur),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const cappedPercentage = (percent: number, cap: number): EstimatedReward => ({
|
|
18
|
+
payoutType: "percentage",
|
|
19
|
+
percent,
|
|
20
|
+
percentOf: "purchase_amount",
|
|
21
|
+
maxAmount: amount(cap),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const uncappedPercentage = (percent: number): EstimatedReward => ({
|
|
25
|
+
payoutType: "percentage",
|
|
26
|
+
percent,
|
|
27
|
+
percentOf: "purchase_amount",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("getRewardValue", () => {
|
|
31
|
+
it("returns the fixed amount in the requested currency", () => {
|
|
32
|
+
expect(getRewardValue(fixed(5), "eurAmount")).toBe(5);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("uses the capped maxAmount for a percentage reward", () => {
|
|
36
|
+
expect(getRewardValue(cappedPercentage(8, 4.8), "eurAmount")).toBe(4.8);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns 0 for an uncapped percentage reward", () => {
|
|
40
|
+
expect(getRewardValue(uncappedPercentage(8), "eurAmount")).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns the richest token tier for a tiered reward", () => {
|
|
44
|
+
expect(
|
|
45
|
+
getRewardValue(
|
|
46
|
+
{
|
|
47
|
+
payoutType: "tiered",
|
|
48
|
+
tierField: "purchase.amount",
|
|
49
|
+
tiers: [
|
|
50
|
+
{ minValue: 0, maxValue: 50, amount: amount(2) },
|
|
51
|
+
{ minValue: 50, amount: amount(9) },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
"eurAmount"
|
|
55
|
+
)
|
|
56
|
+
).toBe(9);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("getRewardRank", () => {
|
|
61
|
+
it("ranks a reward with real money by its money value", () => {
|
|
62
|
+
expect(getRewardRank(fixed(5), "eurAmount")).toBe(5);
|
|
63
|
+
expect(getRewardRank(cappedPercentage(8, 4.8), "eurAmount")).toBe(4.8);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("gives an uncapped percentage a positive weight (never buried at 0)", () => {
|
|
67
|
+
expect(
|
|
68
|
+
getRewardRank(uncappedPercentage(8), "eurAmount")
|
|
69
|
+
).toBeGreaterThan(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("keeps real money ranked above any uncapped percentage", () => {
|
|
73
|
+
expect(getRewardRank(fixed(1), "eurAmount")).toBeGreaterThan(
|
|
74
|
+
getRewardRank(uncappedPercentage(50), "eurAmount")
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("keeps an uncapped percentage ranked above a zero-value reward", () => {
|
|
79
|
+
expect(
|
|
80
|
+
getRewardRank(uncappedPercentage(8), "eurAmount")
|
|
81
|
+
).toBeGreaterThan(getRewardRank(fixed(0), "eurAmount"));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("ranks a higher uncapped percentage above a lower one", () => {
|
|
85
|
+
expect(
|
|
86
|
+
getRewardRank(uncappedPercentage(20), "eurAmount")
|
|
87
|
+
).toBeGreaterThan(getRewardRank(uncappedPercentage(5), "eurAmount"));
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { EstimatedReward, TokenAmountType } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Comparable fiat value of a single reward, used to rank rewards against each
|
|
5
|
+
* other.
|
|
6
|
+
*
|
|
7
|
+
* - `fixed` → the token amount in the requested currency
|
|
8
|
+
* - `tiered` → the highest token amount across tiers
|
|
9
|
+
* - `percentage` → the capped (`maxAmount`) value when present, otherwise `0`
|
|
10
|
+
* (an uncapped percentage has no comparable fiat value)
|
|
11
|
+
*/
|
|
12
|
+
export function getRewardValue(
|
|
13
|
+
reward: EstimatedReward,
|
|
14
|
+
key: keyof TokenAmountType
|
|
15
|
+
): number {
|
|
16
|
+
switch (reward.payoutType) {
|
|
17
|
+
case "fixed":
|
|
18
|
+
return reward.amount[key];
|
|
19
|
+
case "percentage":
|
|
20
|
+
return reward.maxAmount?.[key] ?? 0;
|
|
21
|
+
case "tiered":
|
|
22
|
+
return reward.tiers.reduce(
|
|
23
|
+
(max, tier) =>
|
|
24
|
+
"amount" in tier ? Math.max(max, tier.amount[key]) : max,
|
|
25
|
+
0
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Highest percent a reward exposes: the flat `percentage` percent, or the
|
|
32
|
+
* richest percent across `tiered` tiers, or `0` when it carries none.
|
|
33
|
+
*
|
|
34
|
+
* Shared by {@link getRewardRank} (to weight a percentage-only reward) and
|
|
35
|
+
* `formatEstimatedReward` (to render it as `"X %"`), so the value the UI ranks
|
|
36
|
+
* by and the value it prints stay derived from one tier traversal.
|
|
37
|
+
*/
|
|
38
|
+
export function maxRewardPercent(reward: EstimatedReward): number {
|
|
39
|
+
if (reward.payoutType === "percentage") return reward.percent;
|
|
40
|
+
if (reward.payoutType === "tiered") {
|
|
41
|
+
return reward.tiers.reduce(
|
|
42
|
+
(max, tier) =>
|
|
43
|
+
"percent" in tier ? Math.max(max, tier.percent) : max,
|
|
44
|
+
0
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A reward with no money value (an uncapped percentage, or a percent-only
|
|
51
|
+
// tier set) still renders as "X %", so it is worth surfacing. We give it a
|
|
52
|
+
// positive ranking weight derived from its percent but scaled far below any
|
|
53
|
+
// real-money reward, so the two invariants the UI relies on hold: (1) a reward
|
|
54
|
+
// with real money always outranks a percentage-only reward, and (2) a
|
|
55
|
+
// percentage-only reward still outranks a zero-value reward. Together they
|
|
56
|
+
// guarantee the reward the ranking picks is always one we can display.
|
|
57
|
+
const PERCENT_ONLY_RANK_WEIGHT = 1e-6;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Ranking weight used to pick the single most attractive reward to surface.
|
|
61
|
+
*
|
|
62
|
+
* Mirrors {@link getRewardValue} (money value) but lifts a percentage-only
|
|
63
|
+
* reward to a tiny positive weight instead of `0`, so it is never buried
|
|
64
|
+
* behind a zero-value reward when choosing what to display.
|
|
65
|
+
*/
|
|
66
|
+
export function getRewardRank(
|
|
67
|
+
reward: EstimatedReward,
|
|
68
|
+
key: keyof TokenAmountType
|
|
69
|
+
): number {
|
|
70
|
+
const value = getRewardValue(reward, key);
|
|
71
|
+
if (value > 0) return value;
|
|
72
|
+
return maxRewardPercent(reward) * PERCENT_ONLY_RANK_WEIGHT;
|
|
73
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -53,9 +53,15 @@ export type {
|
|
|
53
53
|
} from "./rpc/embedded";
|
|
54
54
|
export type { SendInteractionParamsType } from "./rpc/interaction";
|
|
55
55
|
export type {
|
|
56
|
+
ConditionGroup,
|
|
57
|
+
ConditionOperator,
|
|
56
58
|
EstimatedReward,
|
|
57
59
|
GetMerchantInformationReturnType,
|
|
60
|
+
MerchantReward,
|
|
58
61
|
RewardTier,
|
|
62
|
+
RuleCondition,
|
|
63
|
+
RuleConditions,
|
|
64
|
+
RuleField,
|
|
59
65
|
TokenAmountType,
|
|
60
66
|
} from "./rpc/merchantInformation";
|
|
61
67
|
export type {
|
|
@@ -43,6 +43,8 @@ export type ResolvedPlacement = {
|
|
|
43
43
|
referrerNoRewardText?: string;
|
|
44
44
|
ctaText?: string;
|
|
45
45
|
ctaNoRewardText?: string;
|
|
46
|
+
/** Custom illustration URL replacing the built-in gift icon. */
|
|
47
|
+
imageUrl?: string;
|
|
46
48
|
css?: string;
|
|
47
49
|
};
|
|
48
50
|
banner?: {
|
|
@@ -52,6 +54,8 @@ export type ResolvedPlacement = {
|
|
|
52
54
|
inappTitle?: string;
|
|
53
55
|
inappDescription?: string;
|
|
54
56
|
inappCta?: string;
|
|
57
|
+
/** Custom illustration URL replacing the built-in gift icon. */
|
|
58
|
+
imageUrl?: string;
|
|
55
59
|
css?: string;
|
|
56
60
|
};
|
|
57
61
|
};
|
|
@@ -52,6 +52,112 @@ export type EstimatedReward =
|
|
|
52
52
|
tiers: RewardTier[];
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Comparison operators usable in a {@link RuleCondition}.
|
|
57
|
+
* @group RPC Schema
|
|
58
|
+
*/
|
|
59
|
+
export type ConditionOperator =
|
|
60
|
+
| "eq"
|
|
61
|
+
| "neq"
|
|
62
|
+
| "gt"
|
|
63
|
+
| "gte"
|
|
64
|
+
| "lt"
|
|
65
|
+
| "lte"
|
|
66
|
+
| "in"
|
|
67
|
+
| "not_in"
|
|
68
|
+
| "contains"
|
|
69
|
+
| "starts_with"
|
|
70
|
+
| "ends_with"
|
|
71
|
+
| "exists"
|
|
72
|
+
| "not_exists"
|
|
73
|
+
| "between";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Dot-path of the rule-evaluation context field a {@link RuleCondition} targets.
|
|
77
|
+
*
|
|
78
|
+
* Only the paths the SDK actually reads are listed (for editor autocompletion);
|
|
79
|
+
* the trailing `string` member keeps the type open to any other path the backend
|
|
80
|
+
* may emit, so it never lies at runtime. Custom interaction data is addressed
|
|
81
|
+
* through `custom.${string}`.
|
|
82
|
+
* @group RPC Schema
|
|
83
|
+
*/
|
|
84
|
+
export type RuleField =
|
|
85
|
+
// Minimum-purchase gating (read by `extractMinPurchaseAmount`)
|
|
86
|
+
| "purchase.amount"
|
|
87
|
+
// Campaign start gating (read by `extractStartDate`)
|
|
88
|
+
| "time.timestamp"
|
|
89
|
+
// Referrer attribution
|
|
90
|
+
| "attribution.referrerIdentityGroupId"
|
|
91
|
+
// Custom interaction context
|
|
92
|
+
| `custom.${string}`
|
|
93
|
+
// Escape hatch — any other context path (kept assignable to/from `string`)
|
|
94
|
+
| (string & Record<never, never>);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A single leaf rule condition. Compares the value found at {@link RuleField}
|
|
98
|
+
* in the evaluation context against `value` (and `valueTo` for `between`).
|
|
99
|
+
* @group RPC Schema
|
|
100
|
+
*/
|
|
101
|
+
export type RuleCondition = {
|
|
102
|
+
field: RuleField;
|
|
103
|
+
operator: ConditionOperator;
|
|
104
|
+
value: string | number | boolean | null;
|
|
105
|
+
valueTo?: string | number | boolean | null;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A recursive group of conditions combined through a boolean `logic`.
|
|
110
|
+
* @group RPC Schema
|
|
111
|
+
*/
|
|
112
|
+
export type ConditionGroup = {
|
|
113
|
+
logic: "all" | "any" | "none";
|
|
114
|
+
conditions: (RuleCondition | ConditionGroup)[];
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Campaign gating rules: a flat list of {@link RuleCondition} (implicitly
|
|
119
|
+
* AND-ed) or a nested {@link ConditionGroup} tree. Surfaced raw so integrators
|
|
120
|
+
* can inspect the rules and derive their own display (start date, minimum
|
|
121
|
+
* purchase, …) instead of relying on pre-computed fields.
|
|
122
|
+
* @group RPC Schema
|
|
123
|
+
*/
|
|
124
|
+
export type RuleConditions = RuleCondition[] | ConditionGroup;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A reward offer exposed by a merchant campaign.
|
|
128
|
+
*
|
|
129
|
+
* Mirrors the backend `EstimatedRewardItem` one-to-one — a static parity
|
|
130
|
+
* assertion on the backend keeps its runtime-validated schema in lockstep with
|
|
131
|
+
* this published contract.
|
|
132
|
+
* @group RPC Schema
|
|
133
|
+
*/
|
|
134
|
+
export type MerchantReward = {
|
|
135
|
+
/** Reward token address; falls back to the merchant token when omitted. */
|
|
136
|
+
token?: Address;
|
|
137
|
+
/** Identifier of the campaign rule this reward originates from. */
|
|
138
|
+
campaignId: string;
|
|
139
|
+
/** Campaign display name. */
|
|
140
|
+
name: string;
|
|
141
|
+
/** Interaction that triggers the reward. */
|
|
142
|
+
interactionTypeKey: InteractionTypeKey;
|
|
143
|
+
/** Reward paid to the referrer, when the campaign defines one. */
|
|
144
|
+
referrer?: EstimatedReward;
|
|
145
|
+
/** Reward paid to the referee, when the campaign defines one. */
|
|
146
|
+
referee?: EstimatedReward;
|
|
147
|
+
/** Raw gating rules — inspect to derive start date, minimum purchase, … */
|
|
148
|
+
conditions: RuleConditions;
|
|
149
|
+
/** Seconds a reward stays locked before settlement. */
|
|
150
|
+
defaultLockupSeconds?: number;
|
|
151
|
+
/** Days before a pending reward expires. */
|
|
152
|
+
pendingRewardExpirationDays?: number;
|
|
153
|
+
/** Per-user reward cap for this campaign. */
|
|
154
|
+
maxRewardsPerUser?: number;
|
|
155
|
+
/** Merchant-wide per-user reward cap across every campaign. */
|
|
156
|
+
merchantMaxRewardsPerUser?: number;
|
|
157
|
+
/** ISO-8601 campaign end date, or `null` when open-ended. */
|
|
158
|
+
expiresAt?: string | null;
|
|
159
|
+
};
|
|
160
|
+
|
|
55
161
|
/**
|
|
56
162
|
* Response of the `frak_getMerchantInformation` RPC method
|
|
57
163
|
* @group RPC Schema
|
|
@@ -74,11 +180,5 @@ export type GetMerchantInformationReturnType = {
|
|
|
74
180
|
*/
|
|
75
181
|
domain: string;
|
|
76
182
|
};
|
|
77
|
-
rewards:
|
|
78
|
-
token?: Address;
|
|
79
|
-
campaignId: string;
|
|
80
|
-
interactionTypeKey: InteractionTypeKey;
|
|
81
|
-
referrer?: EstimatedReward;
|
|
82
|
-
referee?: EstimatedReward;
|
|
83
|
-
}[];
|
|
183
|
+
rewards: MerchantReward[];
|
|
84
184
|
};
|
package/dist/src-DXE40XuK.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{_ as e,g as t,h as n,p as r,y as i}from"./frakContext-Bgidv-Se.js";import{Deferred as a,FrakRpcError as o,RpcErrorCodes as s,createRpcClient as c,jsonDecode as l}from"@frak-labs/frame-connector";import{OpenPanel as u}from"@openpanel/web";const d=`nexus-wallet-backup`,f=`frakwallet://`;function p(e,t){if(typeof window>`u`)return;let n=new URL(window.location.href),r=n.searchParams.get(`sso`);r&&(t.then(()=>{e.sendLifecycle({clientLifecycle:`sso-redirect-complete`,data:{compressed:r}}),console.log(`[SSO URL Listener] Forwarded compressed SSO data to iframe`)}).catch(e=>{console.error(`[SSO URL Listener] Failed to forward SSO data:`,e)}),n.searchParams.delete(`sso`),window.history.replaceState({},``,n.toString()),console.log(`[SSO URL Listener] SSO parameter detected and URL cleaned`))}function m(){let e=navigator.userAgent;return/Android/i.test(e)&&/Chrome\/\d+/i.test(e)}const h=f.replace(`://`,``);function g(e){return`intent://${e.slice(13)}#Intent;scheme=${h};end`}function _(e,t){let n=t?.timeout??2500,r=!1,i=()=>{document.hidden&&(r=!0)};document.addEventListener(`visibilitychange`,i);let a=m()&&v(e)?g(e):e;window.location.href=a,setTimeout(()=>{document.removeEventListener(`visibilitychange`,i),r||t?.onFallback?.()},n)}function v(e){return e.startsWith(f)}function y(){if(typeof navigator>`u`)return!1;let e=navigator.userAgent;return!!(/iPhone|iPad|iPod/i.test(e)||/Macintosh/i.test(e)&&navigator.maxTouchPoints>1)}const b=y();function x(){return typeof navigator>`u`?!1:b?!0:/Android|webOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)}function S(){if(typeof navigator>`u`)return!1;let e=navigator.userAgent.toLowerCase();return e.includes(`instagram`)||e.includes(`fban`)||e.includes(`fbav`)||e.includes(`facebook`)}const C=S();function w(e){b&&e.startsWith(`https://`)?window.location.href=`x-safari-https://${e.slice(8)}`:b&&e.startsWith(`http://`)?window.location.href=`x-safari-http://${e.slice(7)}`:window.location.href=`https://backend.frak.id/common/social?u=${encodeURIComponent(e)}`}const T={id:`frak-wallet`,name:`frak-wallet`,title:`Frak Wallet`,allow:`publickey-credentials-get *; clipboard-write; web-share *`,style:{width:`0`,height:`0`,border:`0`,position:`absolute`,zIndex:2000001,top:`-1000px`,left:`-1000px`,colorScheme:`auto`}};function E({walletBaseUrl:e,config:n}){let r=document.querySelector(`#frak-wallet`);r&&r.remove();let a=document.createElement(`iframe`);a.id=T.id,a.name=T.name,a.allow=T.allow,a.style.zIndex=T.style.zIndex.toString(),O({iframe:a,isVisible:!1});let o=n?.walletUrl??e??`https://wallet.frak.id`,s=i();return A(o),A(t(o)),a.src=D({walletUrl:o,clientId:s,preload:n?.preload}),new Promise(e=>{a.addEventListener(`load`,()=>e(a)),document.body.appendChild(a)})}function D({walletUrl:e,clientId:t,preload:n}){let r=`${e}/listener?clientId=${encodeURIComponent(t)}`;return!n||n.length===0?r:`${r}#preload=${n.join(`,`)}`}function O({iframe:e,isVisible:t}){if(!t){e.style.width=`0`,e.style.height=`0`,e.style.border=`0`,e.style.position=`fixed`,e.style.top=`-1000px`,e.style.left=`-1000px`;return}e.style.position=`fixed`,e.style.top=`0`,e.style.left=`0`,e.style.width=`100%`,e.style.height=`100%`,e.style.pointerEvents=`auto`}function k(e=`/listener`){if(!window.opener)return null;let t=t=>{try{return t.location.origin===window.location.origin&&t.location.pathname===e}catch{return!1}};if(t(window.opener))return window.opener;try{let e=window.opener.frames;for(let n=0;n<e.length;n++)if(t(e[n]))return e[n];return null}catch(t){return console.error(`[findIframeInOpener] Error finding iframe with pathname ${e}:`,t),null}}function A(e){if(!(typeof document>`u`))try{let t=new URL(e).origin,n=`link[rel="preconnect"][data-frak-preconnect="${t}"]`;if(document.head.querySelector(n))return;let r=document.createElement(`link`);r.rel=`preconnect`,r.href=t,r.crossOrigin=``,r.dataset.frakPreconnect=t,document.head.appendChild(r)}catch{}}const j=b&&C;function M(e){e?localStorage.setItem(d,e):localStorage.removeItem(d)}function N(e,t){try{let n=new URL(e);if(!n.searchParams.has(`u`))return e;let r=I(window.location.href,t);return n.searchParams.delete(`u`),n.searchParams.append(`u`,r),n.toString()}catch{return e}}function P(e){let t=new URL(window.location.href);e&&t.searchParams.set(`fmt`,e);let n=t.protocol===`http:`?`x-safari-http`:`x-safari-https`;window.location.href=`${n}://${t.host}${t.pathname}${t.search}${t.hash}`}function F(e){return e.includes(`/common/social`)}function I(e,t){if(!t)return e;try{let n=new URL(e);return n.searchParams.set(`fmt`,t),n.toString()}catch{return`${e}${e.includes(`?`)?`&`:`?`}fmt=${encodeURIComponent(t)}`}}function L(e,t,n,r,i){if(i){let e=N(t,r);window.open(e,`_blank`);return}if(v(t)){let i=N(t,r);_(i,{onFallback:()=>{e.contentWindow?.postMessage({clientLifecycle:`deep-link-failed`,data:{originalUrl:i}},n)}})}else if(j&&F(t))P(r);else{let e=N(t,r);window.location.href=e}}function R({iframe:e,targetOrigin:t}){let n=new a;return{handleEvent:r=>{if(!(`iframeLifecycle`in r))return;let{iframeLifecycle:i,data:a}=r;switch(i){case`connected`:n.resolve(!0);break;case`do-backup`:M(a.backup);break;case`remove-backup`:localStorage.removeItem(d);break;case`show`:case`hide`:O({iframe:e,isVisible:i===`show`});break;case`redirect`:L(e,a.baseRedirectUrl,t,a.mergeToken,a.openInNewTab);break}},isConnected:n.promise}}function z({config:t,iframe:r}){let l=t?.walletUrl??`https://wallet.frak.id`,d=typeof navigator<`u`?navigator.language?.split(`-`)[0]:void 0,f=t.metadata.lang??(d===`en`||d===`fr`?d:void 0),p=t.domain??(typeof window<`u`?window.location.hostname:``);n.setCacheScope(p,f),n.reset();let m=n.isCacheFresh?void 0:n.resolve(t.domain,t.walletUrl,f),h=R({iframe:r,targetOrigin:l}),g=new a,_=Date.now();if(!r.contentWindow)throw new o(s.configError,`The iframe does not have a content window`);let v=c({emittingTransport:r.contentWindow,listeningTransport:window,targetOrigin:l,middleware:[{async onRequest(e,t){if(!await h.isConnected)throw new o(s.clientNotConnected,`The iframe provider isn't connected yet`);return await g.promise,t}}],lifecycleHandlers:{iframeLifecycle:(e,t)=>{h.handleEvent(e)}}}),y=B(v,h),b=async()=>{y(),v.cleanup(),r.remove(),e(),n.clearCache(),n.reset()},x;{console.log(`[Frak SDK] Initializing OpenPanel`),x=new u({apiUrl:`https://op-api.gcp.frak.id`,clientId:`f305d11d-b93b-487c-80d4-92deb7903e98`,trackScreenViews:!0,trackOutgoingLinks:!0,trackAttributes:!1,filter:({type:e,payload:t})=>(e!==`track`||!t?.properties||`sdk_version`in t.properties||(t.properties={...t.properties,sdk_version:`1.1.5`,user_anonymous_client_id:i()}),!0)}),x.setGlobalProperties({sdk_version:`1.1.5`,user_anonymous_client_id:i()}),x.init(),x.track(`sdk_initialized`,{sdk_version:`1.1.5`});let e=!1,t=setTimeout(()=>{e||(e=!0,x?.track(`sdk_iframe_handshake_failed`,{reason:`timeout`}))},3e4);h.isConnected.then(()=>{e||(e=!0,clearTimeout(t),x?.track(`sdk_iframe_connected`,{handshake_duration_ms:Date.now()-_}))}).catch(()=>{e||(e=!0,clearTimeout(t),x?.track(`sdk_iframe_handshake_failed`,{reason:`unknown`}))})}let S=V({config:t,rpcClient:v,lifecycleManager:h,configPromise:m,contextSent:g,openPanel:x}).then(()=>{}).catch(e=>{throw g.reject(e),e});return{config:t,waitForConnection:h.isConnected,waitForSetup:S,request:v.request,listenerRequest:v.listen,destroy:b,openPanel:x}}function B(e,t){let n,r,i=()=>e.sendLifecycle({clientLifecycle:`heartbeat`});async function a(){i(),n=setInterval(i,250),r=setTimeout(()=>{o(),console.log(`Heartbeat timeout: connection failed`)},3e4),await t.isConnected,o()}function o(){n&&clearInterval(n),r&&clearTimeout(r)}return a(),o}async function V({config:e,rpcClient:t,lifecycleManager:r,configPromise:a,contextSent:o,openPanel:s}){await r.isConnected,p(t,r.isConnected);let c=new URL(window.location.href),l=c.searchParams.get(`fmt`)??void 0;l&&(c.searchParams.delete(`fmt`),window.history.replaceState({},``,c.toString()));let u=t=>{let r=t?.merchantId??e.metadata.merchantId??``,i=t?.domain??``,a=t?.allowedDomains??[],o=t?.sdkConfig,s=o?.attribution||e.attribution?{...e.attribution,...o?.attribution}:void 0;n.setConfig(o?{isResolved:!0,merchantId:r,domain:i,allowedDomains:a,hasRawSdkConfig:!0,name:o.name??e.metadata.name,logoUrl:o.logoUrl??e.metadata.logoUrl,homepageLink:o.homepageLink??e.metadata.homepageLink,lang:o.lang??e.metadata.lang,currency:o.currency??e.metadata.currency,hidden:o.hidden,css:o.css,translations:o.translations,placements:o.placements,components:o.components,attribution:s}:{isResolved:!0,merchantId:r,domain:i,allowedDomains:a,name:e.metadata.name,logoUrl:e.metadata.logoUrl,homepageLink:e.metadata.homepageLink,lang:e.metadata.lang,currency:e.metadata.currency,attribution:s})},f=!1,m=e=>{let n=f?void 0:l;f=!0;let r=e.hasRawSdkConfig?{name:e.name,logoUrl:e.logoUrl,homepageLink:e.homepageLink,lang:e.lang,currency:e.currency,hidden:e.hidden,css:e.css,translations:e.translations,placements:e.placements,attribution:e.attribution}:e.attribution?{attribution:e.attribution}:void 0,a=i();if(s){let t=s.global??{};s.setGlobalProperties({...t,merchant_id:e.merchantId,domain:e.domain??``})}t.sendLifecycle({clientLifecycle:`resolved-config`,data:{merchantId:e.merchantId,domain:e.domain??``,allowedDomains:e.allowedDomains??[],sourceUrl:window.location.href,...a&&{sdkAnonymousId:a},...n&&{pendingMergeToken:n},...r&&{sdkConfig:r}}})};n.isResolved&&(m(n.getConfig()),o.resolve()),a&&(u(await a),m(n.getConfig()),o.resolve());async function h(){let n=e.customizations?.css;n&&t.sendLifecycle({clientLifecycle:`modal-css`,data:{cssLink:n}})}async function g(){let n=e.customizations?.i18n;n&&t.sendLifecycle({clientLifecycle:`modal-i18n`,data:{i18n:n}})}async function _(){if(typeof window>`u`)return;let e=window.localStorage.getItem(d);e&&t.sendLifecycle({clientLifecycle:`restore-backup`,data:{backup:e}})}(await Promise.allSettled([h(),g(),_()])).some(e=>e.status===`rejected`)&&s?.track(`sdk_iframe_handshake_failed`,{reason:`asset_push`})}function H(e){return l(r(e))}const U={eur:`fr-FR`,usd:`en-US`,gbp:`en-GB`};function W(e){return e&&e in U?e:`eur`}function G(e){return e?U[e]??U.eur:U.eur}function K(e,t){let n=G(t),r=W(t);return e.toLocaleString(n,{style:`currency`,currency:r,minimumFractionDigits:0,maximumFractionDigits:2})}function q(e){return e?`${e}Amount`:`eurAmount`}async function J({config:e}){let t=Y(e),n=await E({config:t});if(!n){console.error(`Failed to create iframe`);return}let r=z({config:t,iframe:n});if(await r.waitForSetup,!await r.waitForConnection){console.error(`Failed to connect to client`);return}return r}function Y(e){let t=W(e.metadata?.currency);return{...e,metadata:{...e.metadata,currency:t}}}function X({perCall:e,defaults:t,productUtmContent:n}){if(e===null)return;let r=e!==void 0,i=t!==void 0&&Object.keys(t).length>0;if(!r&&!i&&!(n!==void 0&&n!==``))return;let a={...t,...e??{}},o=n??e?.utmContent;return o!==void 0&&o!==``?a.utmContent=o:delete a.utmContent,a}export{W as a,T as c,C as d,x as f,f as h,K as i,k as l,_ as m,J as n,H as o,w as p,q as r,z as s,X as t,b as u};
|
|
2
|
-
//# sourceMappingURL=src-DXE40XuK.js.map
|