@frak-labs/core-sdk 1.2.0-beta.ca3911f0 → 1.2.0-beta.dfcc05ea
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-DtPVuk2e.js → actions-CMPlZ7CH.js} +2 -2
- package/dist/{actions-DtPVuk2e.js.map → actions-CMPlZ7CH.js.map} +1 -1
- package/dist/{actions-Cyn4asew.cjs → actions-DwDtd2rq.cjs} +2 -2
- package/dist/{actions-Cyn4asew.cjs.map → actions-DwDtd2rq.cjs.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 +4 -4
- package/dist/bundle.d.ts +4 -4
- package/dist/bundle.js +1 -1
- package/dist/frakContext-BGx4IVAK.cjs +2 -0
- package/dist/{frakContext-CeVlWFcX.cjs.map → frakContext-BGx4IVAK.cjs.map} +1 -1
- package/dist/frakContext-Db64Zo2o.js +2 -0
- package/dist/{frakContext-Bgidv-Se.js.map → frakContext-Db64Zo2o.js.map} +1 -1
- package/dist/{getCurrencyAmountKey-DQcwjJiB.js → getCurrencyAmountKey-C-1W-BRg.js} +1 -1
- package/dist/{getCurrencyAmountKey-DQcwjJiB.js.map → getCurrencyAmountKey-C-1W-BRg.js.map} +1 -1
- package/dist/{getCurrencyAmountKey-h2eOiYzv.cjs → getCurrencyAmountKey-oIVOsRrF.cjs} +2 -2
- package/dist/{getCurrencyAmountKey-h2eOiYzv.cjs.map → getCurrencyAmountKey-oIVOsRrF.cjs.map} +1 -1
- package/dist/{index-Bxos0XKv.d.ts → index-BrMCc4nK.d.cts} +16 -34
- package/dist/{index-DQYipoH_.d.ts → index-DjAbFZuV.d.ts} +9 -28
- package/dist/{index-DPez7SXh.d.cts → index-DlDKFfSX.d.cts} +9 -28
- package/dist/{index-CGoyXIxE.d.cts → index-Dmng11I3.d.ts} +16 -34
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/{merchantInformation--tLnowAN.d.ts → merchantInformation-BFdhF3De.d.cts} +28 -15
- package/dist/{merchantInformation-BSuahpjl.d.cts → merchantInformation-BFdhF3De.d.ts} +28 -15
- package/dist/{openSso-CjZ8lAEA.d.ts → openSso-DYe9Eyet.d.ts} +66 -35
- package/dist/{openSso-DSXbUP-j.d.cts → openSso-NCXikBK6.d.cts} +66 -35
- package/dist/rewards.cjs +1 -1
- package/dist/rewards.cjs.map +1 -1
- package/dist/rewards.d.cts +11 -6
- package/dist/rewards.d.ts +11 -6
- package/dist/rewards.js +1 -1
- package/dist/rewards.js.map +1 -1
- package/dist/{src-DKVdYUwV.js → src-Bigq2qnT.js} +2 -2
- package/dist/{src-DKVdYUwV.js.map → src-Bigq2qnT.js.map} +1 -1
- package/dist/{src-N8U2UQt0.cjs → src-D2D9Cw9X.cjs} +2 -2
- package/dist/{src-N8U2UQt0.cjs.map → src-D2D9Cw9X.cjs.map} +1 -1
- package/package.json +2 -2
- package/dist/frakContext-Bgidv-Se.js +0 -2
- package/dist/frakContext-CeVlWFcX.cjs +0 -2
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { d as InteractionTypeKey, f as Currency, h as Language, i as GetMerchantInformationReturnType, m as I18nConfig, p as FrakWalletSdkConfig, v as AttributionDefaults, y as AttributionParams } from "./merchantInformation-
|
|
1
|
+
import { d as InteractionTypeKey, f as Currency, h as Language, i as GetMerchantInformationReturnType, m as I18nConfig, p as FrakWalletSdkConfig, v as AttributionDefaults, y as AttributionParams } from "./merchantInformation-BFdhF3De.cjs";
|
|
2
2
|
import { OpenPanel } from "@openpanel/web";
|
|
3
3
|
import { Address, Hex } from "viem";
|
|
4
4
|
import { LifecycleMessage, RpcClient } from "@frak-labs/frame-connector";
|
|
5
5
|
import { SiweMessage } from "viem/siwe";
|
|
6
|
-
|
|
7
6
|
//#region src/types/resolvedConfig.d.ts
|
|
8
7
|
/**
|
|
9
8
|
* Response from the merchant resolve endpoint
|
|
@@ -22,7 +21,8 @@ type MerchantConfigResponse = {
|
|
|
22
21
|
* @category Config
|
|
23
22
|
*/
|
|
24
23
|
type ResolvedPlacement = {
|
|
25
|
-
/** Per-component configuration within this placement */
|
|
24
|
+
/** Per-component configuration within this placement */
|
|
25
|
+
components?: {
|
|
26
26
|
buttonShare?: {
|
|
27
27
|
text?: string;
|
|
28
28
|
noRewardText?: string;
|
|
@@ -44,7 +44,8 @@ type ResolvedPlacement = {
|
|
|
44
44
|
referrerText?: string;
|
|
45
45
|
referrerNoRewardText?: string;
|
|
46
46
|
ctaText?: string;
|
|
47
|
-
ctaNoRewardText?: string;
|
|
47
|
+
ctaNoRewardText?: string;
|
|
48
|
+
/** Custom illustration URL replacing the built-in gift icon. */
|
|
48
49
|
imageUrl?: string;
|
|
49
50
|
css?: string;
|
|
50
51
|
};
|
|
@@ -54,13 +55,16 @@ type ResolvedPlacement = {
|
|
|
54
55
|
referralCta?: string;
|
|
55
56
|
inappTitle?: string;
|
|
56
57
|
inappDescription?: string;
|
|
57
|
-
inappCta?: string;
|
|
58
|
+
inappCta?: string;
|
|
59
|
+
/** Custom illustration URL replacing the built-in gift icon. */
|
|
58
60
|
imageUrl?: string;
|
|
59
61
|
css?: string;
|
|
60
62
|
};
|
|
61
63
|
};
|
|
62
|
-
targetInteraction?: string;
|
|
63
|
-
|
|
64
|
+
targetInteraction?: string;
|
|
65
|
+
/** Already flattened: default + lang-specific merged into one record */
|
|
66
|
+
translations?: Record<string, string>;
|
|
67
|
+
/** Global placement CSS (applied to modals/listener) */
|
|
64
68
|
css?: string;
|
|
65
69
|
};
|
|
66
70
|
/**
|
|
@@ -73,11 +77,13 @@ type ResolvedSdkConfig = {
|
|
|
73
77
|
logoUrl?: string;
|
|
74
78
|
homepageLink?: string;
|
|
75
79
|
currency?: Currency;
|
|
76
|
-
lang?: Language;
|
|
80
|
+
lang?: Language;
|
|
81
|
+
/** When true, all SDK components should be hidden */
|
|
77
82
|
hidden?: boolean;
|
|
78
83
|
css?: string;
|
|
79
84
|
translations?: Record<string, string>;
|
|
80
|
-
placements?: Record<string, ResolvedPlacement>;
|
|
85
|
+
placements?: Record<string, ResolvedPlacement>;
|
|
86
|
+
/** Global component defaults (used when no placement override exists) */
|
|
81
87
|
components?: ResolvedPlacement["components"];
|
|
82
88
|
/**
|
|
83
89
|
* Default attribution params applied when building outbound sharing URLs.
|
|
@@ -93,21 +99,33 @@ type ResolvedSdkConfig = {
|
|
|
93
99
|
* @category Config
|
|
94
100
|
*/
|
|
95
101
|
type SdkResolvedConfig = {
|
|
96
|
-
/** Whether the backend config has been resolved */
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
/** Whether the backend config has been resolved */
|
|
103
|
+
isResolved: boolean;
|
|
104
|
+
/** Merchant ID from resolution */
|
|
105
|
+
merchantId: string;
|
|
106
|
+
/** Domain returned by the resolve endpoint */
|
|
107
|
+
domain?: string;
|
|
108
|
+
/** Domains allowed for this merchant (used by iframe trust check) */
|
|
109
|
+
allowedDomains?: string[];
|
|
110
|
+
/** Whether the resolve returned a backend sdkConfig object */
|
|
111
|
+
hasRawSdkConfig?: boolean;
|
|
112
|
+
/** Merged metadata fields */
|
|
101
113
|
name?: string;
|
|
102
114
|
logoUrl?: string;
|
|
103
115
|
homepageLink?: string;
|
|
104
116
|
lang?: Language;
|
|
105
|
-
currency?: Currency;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
currency?: Currency;
|
|
118
|
+
/** When true, all SDK components should be hidden */
|
|
119
|
+
hidden?: boolean;
|
|
120
|
+
/** Global CSS from backend config (passed to iframe) */
|
|
121
|
+
css?: string;
|
|
122
|
+
/** Global translations (for reference / component fallback) */
|
|
123
|
+
translations?: Record<string, string>;
|
|
124
|
+
/** Named placements (keyed by placement ID) */
|
|
125
|
+
placements?: Record<string, ResolvedPlacement>;
|
|
126
|
+
/** Global component defaults (fallback for placement-level overrides) */
|
|
127
|
+
components?: ResolvedPlacement["components"];
|
|
128
|
+
/** Merged attribution defaults: backend > SDK static config */
|
|
111
129
|
attribution?: AttributionDefaults;
|
|
112
130
|
};
|
|
113
131
|
//#endregion
|
|
@@ -154,9 +172,12 @@ type DeepLinkFailedEvent = {
|
|
|
154
172
|
type ResolvedConfigEvent = {
|
|
155
173
|
clientLifecycle: "resolved-config";
|
|
156
174
|
data: {
|
|
157
|
-
merchantId: string;
|
|
158
|
-
|
|
159
|
-
|
|
175
|
+
merchantId: string;
|
|
176
|
+
/** The domain the backend resolved this config for */
|
|
177
|
+
domain: string;
|
|
178
|
+
/** All domains registered for this merchant (for domain proof) */
|
|
179
|
+
allowedDomains: string[];
|
|
180
|
+
/** Full URL of the parent page (for interaction tracking) */
|
|
160
181
|
sourceUrl: string;
|
|
161
182
|
/**
|
|
162
183
|
* Pending merge token extracted from URL (?fmt= parameter).
|
|
@@ -471,7 +492,7 @@ type ModalStepTypes = LoginModalStepType | SiweAuthenticateModalStepType | SendT
|
|
|
471
492
|
*/
|
|
472
493
|
type ModalRpcStepsResultType<T extends ModalStepTypes[] = ModalStepTypes[]> = { [K in T[number]["key"]]: Extract<T[number], {
|
|
473
494
|
key: K;
|
|
474
|
-
}>["returns"] };
|
|
495
|
+
}>["returns"]; };
|
|
475
496
|
/**
|
|
476
497
|
* Type for the RPC input of a modal
|
|
477
498
|
* Just the `params` type of each `ModalStepTypes`
|
|
@@ -481,7 +502,7 @@ type ModalRpcStepsResultType<T extends ModalStepTypes[] = ModalStepTypes[]> = {
|
|
|
481
502
|
*/
|
|
482
503
|
type ModalRpcStepsInput<T extends ModalStepTypes[] = ModalStepTypes[]> = { [K in T[number]["key"]]?: Extract<T[number], {
|
|
483
504
|
key: K;
|
|
484
|
-
}>["params"] };
|
|
505
|
+
}>["params"]; };
|
|
485
506
|
/**
|
|
486
507
|
* RPC metadata for the modal, used on top level modal configuration
|
|
487
508
|
* @group Modal Display
|
|
@@ -758,14 +779,18 @@ type DisplayEmbeddedWalletResultType = {
|
|
|
758
779
|
* @group RPC Schema
|
|
759
780
|
*/
|
|
760
781
|
type SendInteractionParamsType = {
|
|
761
|
-
type: "arrival";
|
|
782
|
+
type: "arrival";
|
|
783
|
+
/** Sharer wallet address. Accepted in both wallet-only legacy contexts and merchant-context (V2) contexts. */
|
|
762
784
|
referrerWallet?: Address;
|
|
763
785
|
referrerClientId?: string;
|
|
764
|
-
referrerMerchantId?: string;
|
|
786
|
+
referrerMerchantId?: string;
|
|
787
|
+
/** Epoch seconds timestamp from the referral link creation */
|
|
765
788
|
referralTimestamp?: number;
|
|
766
789
|
} | {
|
|
767
|
-
type: "sharing";
|
|
768
|
-
|
|
790
|
+
type: "sharing";
|
|
791
|
+
/** Epoch seconds timestamp matching the V2 context `t` field embedded in the referral link URL, used for backend correlation */
|
|
792
|
+
sharingTimestamp?: number;
|
|
793
|
+
/** Merchant order ID linking this sharing event to a purchase (stays server-side, never in URL) */
|
|
769
794
|
purchaseId?: string;
|
|
770
795
|
} | {
|
|
771
796
|
type: "custom";
|
|
@@ -1021,7 +1046,8 @@ type FrakClient = {
|
|
|
1021
1046
|
* @ignore
|
|
1022
1047
|
*/
|
|
1023
1048
|
type FrakContextV1 = {
|
|
1024
|
-
/** Referrer wallet address */
|
|
1049
|
+
/** Referrer wallet address */
|
|
1050
|
+
r: Address;
|
|
1025
1051
|
};
|
|
1026
1052
|
/**
|
|
1027
1053
|
* V2 Frak Context — anonymous-first referral context with optional wallet.
|
|
@@ -1039,10 +1065,15 @@ type FrakContextV1 = {
|
|
|
1039
1065
|
* @ignore
|
|
1040
1066
|
*/
|
|
1041
1067
|
type FrakContextV2 = {
|
|
1042
|
-
/** Version discriminator */
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1068
|
+
/** Version discriminator */
|
|
1069
|
+
v: 2;
|
|
1070
|
+
/** Merchant ID (UUID) */
|
|
1071
|
+
m: string;
|
|
1072
|
+
/** Link creation timestamp (epoch seconds) */
|
|
1073
|
+
t: number;
|
|
1074
|
+
/** Sharer's anonymous clientId (UUID from localStorage). Optional when `w` is provided. */
|
|
1075
|
+
c?: string;
|
|
1076
|
+
/** Sharer's wallet address. Preferred source of truth when the sharer is authenticated. Optional when `c` is provided. */
|
|
1046
1077
|
w?: Address;
|
|
1047
1078
|
};
|
|
1048
1079
|
/**
|
|
@@ -1114,4 +1145,4 @@ declare const ssoPopupName = "frak-sso";
|
|
|
1114
1145
|
declare function openSso(client: FrakClient, inputArgs: OpenSsoParamsType): Promise<OpenSsoReturnType>;
|
|
1115
1146
|
//#endregion
|
|
1116
1147
|
export { SendTransactionTxType as A, FinalActionType as B, DisplayModalParamsType as C, ModalStepTypes as D, ModalRpcStepsResultType as E, OpenSsoParamsType as F, MerchantConfigResponse as G, ModalStepMetadata as H, OpenSsoReturnType as I, SdkResolvedConfig as J, ResolvedPlacement as K, PrepareSsoParamsType as L, SiweAuthenticateReturnType as M, SiweAuthenticationParams as N, SendTransactionModalStepType as O, LoginModalStepType as P, PrepareSsoReturnType as R, SharingPageProduct as S, ModalRpcStepsInput as T, IFrameLifecycleEvent as U, FinalModalStepType as V, ClientLifecycleEvent as W, EmbeddedViewActionReferred as _, FrakContextV1 as a, DisplaySharingPageParamsType as b, FrakLifecycleEvent as c, WalletStatusReturnType as d, UserReferralStatusType as f, LoggedOutEmbeddedView as g, DisplayEmbeddedWalletResultType as h, FrakContext as i, SiweAuthenticateModalStepType as j, SendTransactionReturnType as k, IFrameTransport as l, DisplayEmbeddedWalletParamsType as m, ssoPopupFeatures as n, FrakContextV2 as o, SendInteractionParamsType as p, ResolvedSdkConfig as q, ssoPopupName as r, FrakClient as s, openSso as t, IFrameRpcSchema as u, EmbeddedViewActionSharing as v, ModalRpcMetadata as w, DisplaySharingPageResultType as x, LoggedInEmbeddedView as y, SsoMetadata as z };
|
|
1117
|
-
//# sourceMappingURL=openSso-
|
|
1148
|
+
//# sourceMappingURL=openSso-NCXikBK6.d.cts.map
|
package/dist/rewards.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./getCurrencyAmountKey-oIVOsRrF.cjs");function t(e){return Array.isArray(e)?e:e.conditions}function n(e){if(typeof e==`number`)return e;if(typeof e==`string`&&e.trim()!==``){let t=Number(e);return Number.isFinite(t)?t:void 0}}function r(e,i,a){let o=[];for(let s of t(e)){if(`logic`in s){o.push(...r(s,i,a));continue}if(s.field===i&&a.has(s.operator)){let e=n(s.value);e!=null&&o.push(e)}}return o}const i=new Set([`gt`,`gte`,`between`]),a=new Set([`gt`,`gte`]);function o(e){let t=r(e,`purchase.amount`,i);return t.length>0?Math.min(...t):void 0}function s(e){let t=r(e,`time.timestamp`,a);if(t.length!==0)return new Date(Math.min(...t)*1e3)}function c(e,t){let n=Math.round((e+t)/2/10)*10;return Math.min(t,Math.max(e,n))}function l(e){return Math.max(100,e??0)}function u(e,t){return t==null?e:e<=100&&100<=t?100:c(e,t)}function d(e,t){let n=l(t);if(n<=0)return;let r=e.percent/100*n;return e.minAmount&&(r=Math.max(r,e.minAmount.eurAmount)),e.maxAmount&&(r=Math.min(r,e.maxAmount.eurAmount)),{basket:n,reward:r}}function f(e,t,n){let r=u(t,n);if(!(r<=0))return{basket:r,reward:e/100*r}}function p(e,t){switch(e.payoutType){case`fixed`:return e.amount[t];case`percentage`:return e.maxAmount?.[t]??0;case`tiered`:return e.tiers.reduce((e,n)=>`amount`in n?Math.max(e,n.amount[t]):e,0)}}function m(e){return e.payoutType===`percentage`?e.percent:e.payoutType===`tiered`?e.tiers.reduce((e,t)=>`percent`in t?Math.max(e,t.percent):e,0):0}function h(e,t){let n=p(e,t);return n>0?n:m(e)*1e-6}function g(t,n){let r=e.r(n),i=e.t(r);switch(t.payoutType){case`fixed`:return e.n(Math.round(t.amount[i]),r);case`percentage`:return`${t.percent} %`;case`tiered`:{let n=p(t,i);if(n>0)return e.n(Math.round(n),r);let a=m(t);return a>0?`${a} %`:e.n(0,r)}}}function _(t,n){if(t&&!(h(t,e.t(e.r(n)))<=0))return g(t,n)}function v(e,t){return t?e.replaceAll(`{REWARD}`,t):e.replaceAll(`{REWARD}`,``)}function y(e,t){return e.expiresAt!=null&&new Date(e.expiresAt).getTime()<=t}function b(e,t){let n=s(e.conditions);return n==null||n.getTime()<=t}function x(e,t){return t===`referee`?e.referee:e.referrer}function S(e,t,n){let r=x(e,n);return r?h(r,t):0}function C(t,n={}){let r=(n.now??new Date).getTime(),i=n.audience??`referrer`,a=e.t(e.r(n.currency)),o=(n.targetInteraction?t.filter(e=>e.interactionTypeKey===n.targetInteraction):t).filter(e=>!y(e,r)),c=o.filter(e=>b(e,r));if(c.length>0)return{campaign:c.reduce((e,t)=>S(t,a,i)>S(e,a,i)?t:e),status:`live`};let l=o.map(e=>({campaign:e,startsAt:s(e.conditions)})).filter(e=>e.startsAt!=null);if(l.length===0)return;let u=l.reduce((e,t)=>t.startsAt.getTime()<e.startsAt.getTime()?t:e);return{campaign:u.campaign,status:`upcoming`,startsAt:u.startsAt}}function w(t,n={}){let r=C(t,n);if(!r)return;let i=x(r.campaign,n.audience??`referrer`);if(!i)return;let a=_(i,n.currency);if(!a)return;let s=o(r.campaign.conditions),c=s==null?void 0:e.n(s,n.currency),l=r.campaign.defaultLockupSeconds,u=l&&l>0?Math.round(l/86400):void 0;return{formatted:a,payoutType:i.payoutType,minPurchaseAmount:c,lockupDurationDays:u,referrerReward:r.campaign.referrer,refereeReward:r.campaign.referee,minPurchaseValue:s??void 0}}function T(e,t={}){return w(e,t)?.formatted}exports.applyRewardPlaceholder=v,exports.buildPercentageExample=d,exports.buildTierExample=f,exports.extractMinPurchaseAmount=o,exports.extractStartDate=s,exports.formatBestReward=T,exports.formatEstimatedReward=g,exports.formatRewardOrHide=_,exports.getRewardValue=p,exports.pickFlatBasket=l,exports.pickTierBasket=u,exports.selectBestReward=w,exports.selectDisplayCampaign=C;
|
|
2
2
|
//# sourceMappingURL=rewards.cjs.map
|
package/dist/rewards.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rewards.cjs","names":["getSupportedCurrency","getCurrencyAmountKey","formatAmount","getCurrencyAmountKey","getSupportedCurrency","formatAmount"],"sources":["../src/rewards/conditions.ts","../src/rewards/example.ts","../src/rewards/value.ts","../src/rewards/format.ts","../src/rewards/select.ts"],"sourcesContent":["import type { ConditionGroup, RuleCondition, RuleConditions } from \"../types\";\n\ntype ConditionNode = RuleCondition | ConditionGroup;\n\nfunction conditionNodes(conditions: RuleConditions): ConditionNode[] {\n return Array.isArray(conditions) ? conditions : conditions.conditions;\n}\n\nfunction conditionValueToNumber(\n value: RuleCondition[\"value\"]\n): number | undefined {\n if (typeof value === \"number\") return value;\n if (typeof value === \"string\" && value.trim() !== \"\") {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n }\n return undefined;\n}\n\n// Conditions arrive as a flat list or recursive groups; collect every numeric\n// value whose field + operator match, descending into nested groups.\nfunction collectConditionValues(\n conditions: RuleConditions,\n field: string,\n operators: ReadonlySet<RuleCondition[\"operator\"]>\n): number[] {\n const values: number[] = [];\n for (const node of conditionNodes(conditions)) {\n if (\"logic\" in node) {\n values.push(...collectConditionValues(node, field, operators));\n continue;\n }\n if (node.field === field && operators.has(node.operator)) {\n const value = conditionValueToNumber(node.value);\n if (value != null) values.push(value);\n }\n }\n return values;\n}\n\nconst MIN_PURCHASE_OPERATORS: ReadonlySet<RuleCondition[\"operator\"]> = new Set([\n \"gt\",\n \"gte\",\n \"between\",\n]);\nconst START_DATE_OPERATORS: ReadonlySet<RuleCondition[\"operator\"]> = new Set([\n \"gt\",\n \"gte\",\n]);\n\nexport function extractMinPurchaseAmount(\n conditions: RuleConditions\n): number | undefined {\n const values = collectConditionValues(\n conditions,\n \"purchase.amount\",\n MIN_PURCHASE_OPERATORS\n );\n return values.length > 0 ? Math.min(...values) : undefined;\n}\n\n// The business app encodes a campaign start as a `time.timestamp >= <unix s>`\n// rule condition; the endpoint returns it verbatim inside `conditions`.\nexport function extractStartDate(conditions: RuleConditions): Date | undefined {\n const values = collectConditionValues(\n conditions,\n \"time.timestamp\",\n START_DATE_OPERATORS\n );\n if (values.length === 0) return undefined;\n return new Date(Math.min(...values) * 1000);\n}\n","import type { EstimatedReward } from \"../types\";\n\ntype PercentageReward = Extract<EstimatedReward, { payoutType: \"percentage\" }>;\n\n// Reference order total used to turn a \"% of basket\" reward into a concrete\n// example amount. The campaign's minimum purchase overrides it when higher.\nconst REFERENCE_BASKET = 100;\n\nexport type RewardExample = {\n basket: number;\n reward: number;\n};\n\nfunction roundWithinRange(min: number, max: number): number {\n const rounded = Math.round((min + max) / 2 / 10) * 10;\n return Math.min(max, Math.max(min, rounded));\n}\n\nexport function pickFlatBasket(minPurchase: number | undefined): number {\n return Math.max(REFERENCE_BASKET, minPurchase ?? 0);\n}\n\n// Basket for a percentage tier: the reference when it sits inside the tier\n// range, a round number within the range otherwise, or the lower bound for an\n// open-ended (uncapped) top tier.\nexport function pickTierBasket(\n minValue: number,\n maxValue: number | undefined\n): number {\n if (maxValue == null) return minValue;\n if (minValue <= REFERENCE_BASKET && REFERENCE_BASKET <= maxValue) {\n return REFERENCE_BASKET;\n }\n return roundWithinRange(minValue, maxValue);\n}\n\nexport function buildPercentageExample(\n reward: PercentageReward,\n minPurchase: number | undefined\n): RewardExample | undefined {\n const basket = pickFlatBasket(minPurchase);\n if (basket <= 0) return undefined;\n let amount = (reward.percent / 100) * basket;\n if (reward.minAmount) amount = Math.max(amount, reward.minAmount.eurAmount);\n if (reward.maxAmount) amount = Math.min(amount, reward.maxAmount.eurAmount);\n return { basket, reward: amount };\n}\n\nexport function buildTierExample(\n percent: number,\n minValue: number,\n maxValue: number | undefined\n): RewardExample | undefined {\n const basket = pickTierBasket(minValue, maxValue);\n if (basket <= 0) return undefined;\n return { basket, reward: (percent / 100) * basket };\n}\n","import type { EstimatedReward, TokenAmountType } from \"../types\";\n\n/**\n * Comparable fiat value of a single reward, used to rank rewards against each\n * other.\n *\n * - `fixed` → the token amount in the requested currency\n * - `tiered` → the highest token amount across tiers\n * - `percentage` → the capped (`maxAmount`) value when present, otherwise `0`\n * (an uncapped percentage has no comparable fiat value)\n */\nexport function getRewardValue(\n reward: EstimatedReward,\n key: keyof TokenAmountType\n): number {\n switch (reward.payoutType) {\n case \"fixed\":\n return reward.amount[key];\n case \"percentage\":\n return reward.maxAmount?.[key] ?? 0;\n case \"tiered\":\n return reward.tiers.reduce(\n (max, tier) =>\n \"amount\" in tier ? Math.max(max, tier.amount[key]) : max,\n 0\n );\n }\n}\n\n/**\n * Highest percent a reward exposes: the flat `percentage` percent, or the\n * richest percent across `tiered` tiers, or `0` when it carries none.\n *\n * Shared by {@link getRewardRank} (to weight a percentage-only reward) and\n * `formatEstimatedReward` (to render it as `\"X %\"`), so the value the UI ranks\n * by and the value it prints stay derived from one tier traversal.\n */\nexport function maxRewardPercent(reward: EstimatedReward): number {\n if (reward.payoutType === \"percentage\") return reward.percent;\n if (reward.payoutType === \"tiered\") {\n return reward.tiers.reduce(\n (max, tier) =>\n \"percent\" in tier ? Math.max(max, tier.percent) : max,\n 0\n );\n }\n return 0;\n}\n\n// A reward with no money value (an uncapped percentage, or a percent-only\n// tier set) still renders as \"X %\", so it is worth surfacing. We give it a\n// positive ranking weight derived from its percent but scaled far below any\n// real-money reward, so the two invariants the UI relies on hold: (1) a reward\n// with real money always outranks a percentage-only reward, and (2) a\n// percentage-only reward still outranks a zero-value reward. Together they\n// guarantee the reward the ranking picks is always one we can display.\nconst PERCENT_ONLY_RANK_WEIGHT = 1e-6;\n\n/**\n * Ranking weight used to pick the single most attractive reward to surface.\n *\n * Mirrors {@link getRewardValue} (money value) but lifts a percentage-only\n * reward to a tiny positive weight instead of `0`, so it is never buried\n * behind a zero-value reward when choosing what to display.\n */\nexport function getRewardRank(\n reward: EstimatedReward,\n key: keyof TokenAmountType\n): number {\n const value = getRewardValue(reward, key);\n if (value > 0) return value;\n return maxRewardPercent(reward) * PERCENT_ONLY_RANK_WEIGHT;\n}\n","import type { Currency, EstimatedReward } from \"../types\";\nimport { formatAmount } from \"../utils/format/formatAmount\";\nimport { getCurrencyAmountKey } from \"../utils/format/getCurrencyAmountKey\";\nimport { getSupportedCurrency } from \"../utils/format/getSupportedCurrency\";\nimport { getRewardRank, getRewardValue, maxRewardPercent } from \"./value\";\n\n/**\n * Format an {@link EstimatedReward} into a human-readable string.\n *\n * - `fixed` → e.g. `\"5 €\"`\n * - `percentage` → e.g. `\"10 %\"`\n * - `tiered` → the richest tier: max token amount (e.g. `\"50 €\"`) or, when\n * tiers only carry percentages, the max percent (e.g. `\"10 %\"`)\n *\n * Percentages are always rendered as a `\"X %\"` string: the backend never sends\n * a reference basket, so the reward cannot be resolved to a concrete amount\n * here. Callers that need a worked example use the `example` helpers instead.\n */\nexport function formatEstimatedReward(\n reward: EstimatedReward,\n currency?: Currency\n): string {\n const supportedCurrency = getSupportedCurrency(currency);\n const key = getCurrencyAmountKey(supportedCurrency);\n\n switch (reward.payoutType) {\n case \"fixed\":\n return formatAmount(\n Math.round(reward.amount[key]),\n supportedCurrency\n );\n\n case \"percentage\":\n return `${reward.percent} %`;\n\n case \"tiered\": {\n const maxAmount = getRewardValue(reward, key);\n if (maxAmount > 0) {\n return formatAmount(Math.round(maxAmount), supportedCurrency);\n }\n const maxPercent = maxRewardPercent(reward);\n if (maxPercent > 0) {\n return `${maxPercent} %`;\n }\n return formatAmount(0, supportedCurrency);\n }\n }\n}\n\n/**\n * Format a reward for display, or return `undefined` when it is not worth\n * advertising. Callers rely on `undefined` to hide a badge / fall back to\n * other copy.\n *\n * A reward is hidden only when it carries no displayable value — a `fixed` or\n * `tiered` reward whose money value is `0` (e.g. an unknown token price). An\n * uncapped percentage always renders as `\"X %\"`, since the percent itself is\n * meaningful even without a money value.\n */\nexport function formatRewardOrHide(\n reward: EstimatedReward | undefined,\n currency?: Currency\n): string | undefined {\n if (!reward) return undefined;\n const key = getCurrencyAmountKey(getSupportedCurrency(currency));\n if (getRewardRank(reward, key) <= 0) return undefined;\n return formatEstimatedReward(reward, currency);\n}\n\n/**\n * Replace the `{REWARD}` placeholder in a text string with a reward value.\n * If no reward is provided, returns the text with `{REWARD}` stripped.\n */\nexport function applyRewardPlaceholder(\n text: string,\n reward: string | undefined\n): string {\n return reward\n ? text.replaceAll(\"{REWARD}\", reward)\n : text.replaceAll(\"{REWARD}\", \"\");\n}\n","import type { InteractionTypeKey } from \"../constants/interactionTypes\";\nimport type {\n Currency,\n EstimatedReward,\n MerchantReward,\n TokenAmountType,\n} from \"../types\";\nimport { formatAmount } from \"../utils/format/formatAmount\";\nimport { getCurrencyAmountKey } from \"../utils/format/getCurrencyAmountKey\";\nimport { getSupportedCurrency } from \"../utils/format/getSupportedCurrency\";\nimport { extractMinPurchaseAmount, extractStartDate } from \"./conditions\";\nimport { formatRewardOrHide } from \"./format\";\nimport { getRewardRank } from \"./value\";\n\n/** Reward side a surface cares about: the sharer (`referrer`) or the referred\n * user (`referee`). Drives both campaign ranking and which side is displayed. */\nexport type RewardAudience = \"referrer\" | \"referee\";\n\nexport type DisplayCampaign = {\n campaign: MerchantReward;\n status: \"live\" | \"upcoming\";\n startsAt?: Date;\n};\n\nexport type SelectDisplayCampaignOptions = {\n /** Reference \"now\" for expiry / start gating; defaults to the current time. */\n now?: Date;\n /** Currency the ranking is expressed in; defaults to EUR. */\n currency?: Currency;\n /** When set, only campaigns triggered by this interaction are considered. */\n targetInteraction?: InteractionTypeKey;\n /** Reward side to rank campaigns by; defaults to `\"referrer\"`. */\n audience?: RewardAudience;\n};\n\nfunction isExpired(campaign: MerchantReward, nowMs: number): boolean {\n return (\n campaign.expiresAt != null &&\n new Date(campaign.expiresAt).getTime() <= nowMs\n );\n}\n\nfunction hasStarted(campaign: MerchantReward, nowMs: number): boolean {\n const startsAt = extractStartDate(campaign.conditions);\n return startsAt == null || startsAt.getTime() <= nowMs;\n}\n\nfunction audienceReward(campaign: MerchantReward, audience: RewardAudience) {\n return audience === \"referee\" ? campaign.referee : campaign.referrer;\n}\n\nfunction campaignRank(\n campaign: MerchantReward,\n key: keyof TokenAmountType,\n audience: RewardAudience\n): number {\n const reward = audienceReward(campaign, audience);\n return reward ? getRewardRank(reward, key) : 0;\n}\n\n/**\n * Pick the single campaign a merchant surface should display.\n *\n * Filters out expired (and, when `targetInteraction` is set, non-matching)\n * campaigns, then prefers the highest-ranked *live* campaign — ranked by the\n * `audience` reward side in the requested currency. When none has started yet\n * it falls back to the soonest-starting upcoming campaign (the endpoint does\n * not gate on the start-date condition, so future-start campaigns come\n * through).\n */\nexport function selectDisplayCampaign(\n rewards: readonly MerchantReward[],\n options: SelectDisplayCampaignOptions = {}\n): DisplayCampaign | undefined {\n const nowMs = (options.now ?? new Date()).getTime();\n const audience = options.audience ?? \"referrer\";\n const key = getCurrencyAmountKey(getSupportedCurrency(options.currency));\n\n const matching = options.targetInteraction\n ? rewards.filter(\n (campaign) =>\n campaign.interactionTypeKey === options.targetInteraction\n )\n : rewards;\n const active = matching.filter((campaign) => !isExpired(campaign, nowMs));\n\n const live = active.filter((campaign) => hasStarted(campaign, nowMs));\n if (live.length > 0) {\n const best = live.reduce((a, b) =>\n campaignRank(b, key, audience) > campaignRank(a, key, audience)\n ? b\n : a\n );\n return { campaign: best, status: \"live\" };\n }\n\n const upcoming = active\n .map((campaign) => ({\n campaign,\n startsAt: extractStartDate(campaign.conditions),\n }))\n .filter(\n (entry): entry is { campaign: MerchantReward; startsAt: Date } =>\n entry.startsAt != null\n );\n if (upcoming.length === 0) return undefined;\n\n const soonest = upcoming.reduce((a, b) =>\n b.startsAt.getTime() < a.startsAt.getTime() ? b : a\n );\n return {\n campaign: soonest.campaign,\n status: \"upcoming\",\n startsAt: soonest.startsAt,\n };\n}\n\n/**\n * The single reward a merchant surface should display: its formatted string\n * plus the `payoutType` of the underlying reward, so surfaces can adapt their\n * presentation (e.g. hide percentage rewards, prefix tiered ones with \"Up to\").\n */\nexport type BestReward = {\n /** Display-ready reward string (e.g. `\"5 €\"`, `\"10 %\"`). */\n formatted: string;\n /** Payout type of the selected reward. */\n payoutType: EstimatedReward[\"payoutType\"];\n /**\n * Minimum purchase amount gating the reward, formatted with the requested\n * currency (e.g. `\"10 €\"`), or `undefined` when the campaign sets no\n * minimum.\n */\n minPurchaseAmount?: string;\n /**\n * Whole-day lockup applied before the reward settles, or `undefined` when\n * the campaign has no lockup.\n */\n lockupDurationDays?: number;\n /**\n * Raw referrer/referee rewards of the selected campaign, surfaced so\n * consumers can render the full per-audience breakdown (tier rows,\n * percentage examples) rather than only the headline number.\n */\n referrerReward?: EstimatedReward;\n refereeReward?: EstimatedReward;\n /**\n * Raw minimum purchase value (unformatted), used to build percentage\n * worked-examples consistent with the campaign's gating.\n */\n minPurchaseValue?: number;\n};\n\n/**\n * Pick the best campaign for `options` and resolve its `audience`-side reward\n * to a formatted string plus its `payoutType`, or `undefined` when there is\n * nothing worth showing.\n *\n * Single entry point shared by every \"headline reward\" surface (share button,\n * wallet modal, sharing/install screens) so they all show the same number for\n * a given merchant and can branch on the payout type.\n */\nexport function selectBestReward(\n rewards: readonly MerchantReward[],\n options: SelectDisplayCampaignOptions = {}\n): BestReward | undefined {\n const selected = selectDisplayCampaign(rewards, options);\n if (!selected) return undefined;\n const reward = audienceReward(\n selected.campaign,\n options.audience ?? \"referrer\"\n );\n if (!reward) return undefined;\n const formatted = formatRewardOrHide(reward, options.currency);\n if (!formatted) return undefined;\n\n const minPurchase = extractMinPurchaseAmount(selected.campaign.conditions);\n const minPurchaseAmount =\n minPurchase != null\n ? formatAmount(minPurchase, options.currency)\n : undefined;\n\n const lockupSeconds = selected.campaign.defaultLockupSeconds;\n const lockupDurationDays =\n lockupSeconds && lockupSeconds > 0\n ? Math.round(lockupSeconds / 86_400)\n : undefined;\n\n return {\n formatted,\n payoutType: reward.payoutType,\n minPurchaseAmount,\n lockupDurationDays,\n referrerReward: selected.campaign.referrer,\n refereeReward: selected.campaign.referee,\n minPurchaseValue: minPurchase ?? undefined,\n };\n}\n\n/**\n * Headline reward string for a merchant: picks the best campaign for `options`\n * and formats its `audience`-side reward, or returns `undefined` when there is\n * nothing worth showing.\n *\n * Thin wrapper over {@link selectBestReward} for callers that only need the\n * formatted string.\n */\nexport function formatBestReward(\n rewards: readonly MerchantReward[],\n options: SelectDisplayCampaignOptions = {}\n): string | undefined {\n return selectBestReward(rewards, options)?.formatted;\n}\n"],"mappings":"0HAIA,SAAS,EAAe,EAA6C,CACjE,OAAO,MAAM,QAAQ,CAAU,EAAI,EAAa,EAAW,UAC/D,CAEA,SAAS,EACL,EACkB,CAClB,GAAI,OAAO,GAAU,SAAU,OAAO,EACtC,GAAI,OAAO,GAAU,UAAY,EAAM,KAAK,IAAM,GAAI,CAClD,IAAM,EAAS,OAAO,CAAK,EAC3B,OAAO,OAAO,SAAS,CAAM,EAAI,EAAS,IAAA,EAC9C,CAEJ,CAIA,SAAS,EACL,EACA,EACA,EACQ,CACR,IAAM,EAAmB,CAAC,EAC1B,IAAK,IAAM,KAAQ,EAAe,CAAU,EAAG,CAC3C,GAAI,UAAW,EAAM,CACjB,EAAO,KAAK,GAAG,EAAuB,EAAM,EAAO,CAAS,CAAC,EAC7D,QACJ,CACA,GAAI,EAAK,QAAU,GAAS,EAAU,IAAI,EAAK,QAAQ,EAAG,CACtD,IAAM,EAAQ,EAAuB,EAAK,KAAK,EAC3C,GAAS,MAAM,EAAO,KAAK,CAAK,CACxC,CACJ,CACA,OAAO,CACX,CAEA,MAAM,EAAiE,IAAI,IAAI,CAC3E,KACA,MACA,SACJ,CAAC,EACK,EAA+D,IAAI,IAAI,CACzE,KACA,KACJ,CAAC,EAED,SAAgB,EACZ,EACkB,CAClB,IAAM,EAAS,EACX,EACA,kBACA,CACJ,EACA,OAAO,EAAO,OAAS,EAAI,KAAK,IAAI,GAAG,CAAM,EAAI,IAAA,EACrD,CAIA,SAAgB,EAAiB,EAA8C,CAC3E,IAAM,EAAS,EACX,EACA,iBACA,CACJ,EACI,KAAO,SAAW,EACtB,OAAO,IAAI,KAAK,KAAK,IAAI,GAAG,CAAM,EAAI,GAAI,CAC9C,CC1DA,SAAS,EAAiB,EAAa,EAAqB,CACxD,IAAM,EAAU,KAAK,OAAO,EAAM,GAAO,EAAI,EAAE,EAAI,GACnD,OAAO,KAAK,IAAI,EAAK,KAAK,IAAI,EAAK,CAAO,CAAC,CAC/C,CAEA,SAAgB,EAAe,EAAyC,CACpE,OAAO,KAAK,IAAI,IAAkB,GAAe,CAAC,CACtD,CAKA,SAAgB,EACZ,EACA,EACM,CAKN,OAJI,GAAY,KAAa,EACzB,GAAY,KAAoB,KAAoB,EAC7C,IAEJ,EAAiB,EAAU,CAAQ,CAC9C,CAEA,SAAgB,EACZ,EACA,EACyB,CACzB,IAAM,EAAS,EAAe,CAAW,EACzC,GAAI,GAAU,EAAG,OACjB,IAAI,EAAU,EAAO,QAAU,IAAO,EAGtC,OAFI,EAAO,YAAW,EAAS,KAAK,IAAI,EAAQ,EAAO,UAAU,SAAS,GACtE,EAAO,YAAW,EAAS,KAAK,IAAI,EAAQ,EAAO,UAAU,SAAS,GACnE,CAAE,SAAQ,OAAQ,CAAO,CACpC,CAEA,SAAgB,EACZ,EACA,EACA,EACyB,CACzB,IAAM,EAAS,EAAe,EAAU,CAAQ,EAC5C,QAAU,GACd,MAAO,CAAE,SAAQ,OAAS,EAAU,IAAO,CAAO,CACtD,CC7CA,SAAgB,EACZ,EACA,EACM,CACN,OAAQ,EAAO,WAAf,CACI,IAAK,QACD,OAAO,EAAO,OAAO,GACzB,IAAK,aACD,OAAO,EAAO,YAAY,IAAQ,EACtC,IAAK,SACD,OAAO,EAAO,MAAM,QACf,EAAK,IACF,WAAY,EAAO,KAAK,IAAI,EAAK,EAAK,OAAO,EAAI,EAAI,EACzD,CACJ,CACR,CACJ,CAUA,SAAgB,EAAiB,EAAiC,CAS9D,OARI,EAAO,aAAe,aAAqB,EAAO,QAClD,EAAO,aAAe,SACf,EAAO,MAAM,QACf,EAAK,IACF,YAAa,EAAO,KAAK,IAAI,EAAK,EAAK,OAAO,EAAI,EACtD,CACJ,EAEG,CACX,CAkBA,SAAgB,EACZ,EACA,EACM,CACN,IAAM,EAAQ,EAAe,EAAQ,CAAG,EAExC,OADI,EAAQ,EAAU,EACf,EAAiB,CAAM,EAAI,IACtC,CCtDA,SAAgB,EACZ,EACA,EACM,CACN,IAAM,EAAoBA,EAAAA,EAAqB,CAAQ,EACjD,EAAMC,EAAAA,EAAqB,CAAiB,EAElD,OAAQ,EAAO,WAAf,CACI,IAAK,QACD,OAAOC,EAAAA,EACH,KAAK,MAAM,EAAO,OAAO,EAAI,EAC7B,CACJ,EAEJ,IAAK,aACD,MAAO,GAAG,EAAO,QAAQ,IAE7B,IAAK,SAAU,CACX,IAAM,EAAY,EAAe,EAAQ,CAAG,EAC5C,GAAI,EAAY,EACZ,OAAOA,EAAAA,EAAa,KAAK,MAAM,CAAS,EAAG,CAAiB,EAEhE,IAAM,EAAa,EAAiB,CAAM,EAI1C,OAHI,EAAa,EACN,GAAG,EAAW,IAElBA,EAAAA,EAAa,EAAG,CAAiB,CAC5C,CACJ,CACJ,CAYA,SAAgB,EACZ,EACA,EACkB,CACb,MAED,IAAc,EADND,EAAAA,EAAqBD,EAAAA,EAAqB,CAAQ,CAClC,CAAC,GAAK,GAClC,OAAO,EAAsB,EAAQ,CAAQ,CACjD,CAMA,SAAgB,EACZ,EACA,EACM,CACN,OAAO,EACD,EAAK,WAAW,WAAY,CAAM,EAClC,EAAK,WAAW,WAAY,EAAE,CACxC,CC7CA,SAAS,EAAU,EAA0B,EAAwB,CACjE,OACI,EAAS,WAAa,MACtB,IAAI,KAAK,EAAS,SAAS,EAAE,QAAQ,GAAK,CAElD,CAEA,SAAS,EAAW,EAA0B,EAAwB,CAClE,IAAM,EAAW,EAAiB,EAAS,UAAU,EACrD,OAAO,GAAY,MAAQ,EAAS,QAAQ,GAAK,CACrD,CAEA,SAAS,EAAe,EAA0B,EAA0B,CACxE,OAAO,IAAa,UAAY,EAAS,QAAU,EAAS,QAChE,CAEA,SAAS,EACL,EACA,EACA,EACM,CACN,IAAM,EAAS,EAAe,EAAU,CAAQ,EAChD,OAAO,EAAS,EAAc,EAAQ,CAAG,EAAI,CACjD,CAYA,SAAgB,EACZ,EACA,EAAwC,CAAC,EACd,CAC3B,IAAM,GAAS,EAAQ,KAAO,IAAI,MAAQ,QAAQ,EAC5C,EAAW,EAAQ,UAAY,WAC/B,EAAMG,EAAAA,EAAqBC,EAAAA,EAAqB,EAAQ,QAAQ,CAAC,EAQjE,GANW,EAAQ,kBACnB,EAAQ,OACH,GACG,EAAS,qBAAuB,EAAQ,iBAChD,EACA,GACkB,OAAQ,GAAa,CAAC,EAAU,EAAU,CAAK,CAAC,EAElE,EAAO,EAAO,OAAQ,GAAa,EAAW,EAAU,CAAK,CAAC,EACpE,GAAI,EAAK,OAAS,EAMd,MAAO,CAAE,SALI,EAAK,QAAQ,EAAG,IACzB,EAAa,EAAG,EAAK,CAAQ,EAAI,EAAa,EAAG,EAAK,CAAQ,EACxD,EACA,CAEY,EAAG,OAAQ,MAAO,EAG5C,IAAM,EAAW,EACZ,IAAK,IAAc,CAChB,WACA,SAAU,EAAiB,EAAS,UAAU,CAClD,EAAE,EACD,OACI,GACG,EAAM,UAAY,IAC1B,EACJ,GAAI,EAAS,SAAW,EAAG,OAE3B,IAAM,EAAU,EAAS,QAAQ,EAAG,IAChC,EAAE,SAAS,QAAQ,EAAI,EAAE,SAAS,QAAQ,EAAI,EAAI,CACtD,EACA,MAAO,CACH,SAAU,EAAQ,SAClB,OAAQ,WACR,SAAU,EAAQ,QACtB,CACJ,CA8CA,SAAgB,EACZ,EACA,EAAwC,CAAC,EACnB,CACtB,IAAM,EAAW,EAAsB,EAAS,CAAO,EACvD,GAAI,CAAC,EAAU,OACf,IAAM,EAAS,EACX,EAAS,SACT,EAAQ,UAAY,UACxB,EACA,GAAI,CAAC,EAAQ,OACb,IAAM,EAAY,EAAmB,EAAQ,EAAQ,QAAQ,EAC7D,GAAI,CAAC,EAAW,OAEhB,IAAM,EAAc,EAAyB,EAAS,SAAS,UAAU,EACnE,EACF,GAAe,KAET,IAAA,GADAC,EAAAA,EAAa,EAAa,EAAQ,QAAQ,EAG9C,EAAgB,EAAS,SAAS,qBAClC,EACF,GAAiB,EAAgB,EAC3B,KAAK,MAAM,EAAgB,KAAM,EACjC,IAAA,GAEV,MAAO,CACH,YACA,WAAY,EAAO,WACnB,oBACA,qBACA,eAAgB,EAAS,SAAS,SAClC,cAAe,EAAS,SAAS,QACjC,iBAAkB,GAAe,IAAA,EACrC,CACJ,CAUA,SAAgB,EACZ,EACA,EAAwC,CAAC,EACvB,CAClB,OAAO,EAAiB,EAAS,CAAO,GAAG,SAC/C"}
|
|
1
|
+
{"version":3,"file":"rewards.cjs","names":["getSupportedCurrency","getCurrencyAmountKey","formatAmount","getCurrencyAmountKey","getSupportedCurrency","formatAmount"],"sources":["../src/rewards/conditions.ts","../src/rewards/example.ts","../src/rewards/value.ts","../src/rewards/format.ts","../src/rewards/select.ts"],"sourcesContent":["import type { ConditionGroup, RuleCondition, RuleConditions } from \"../types\";\n\ntype ConditionNode = RuleCondition | ConditionGroup;\n\nfunction conditionNodes(conditions: RuleConditions): ConditionNode[] {\n return Array.isArray(conditions) ? conditions : conditions.conditions;\n}\n\nfunction conditionValueToNumber(\n value: RuleCondition[\"value\"]\n): number | undefined {\n if (typeof value === \"number\") return value;\n if (typeof value === \"string\" && value.trim() !== \"\") {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n }\n return undefined;\n}\n\n// Conditions arrive as a flat list or recursive groups; collect every numeric\n// value whose field + operator match, descending into nested groups.\nfunction collectConditionValues(\n conditions: RuleConditions,\n field: string,\n operators: ReadonlySet<RuleCondition[\"operator\"]>\n): number[] {\n const values: number[] = [];\n for (const node of conditionNodes(conditions)) {\n if (\"logic\" in node) {\n values.push(...collectConditionValues(node, field, operators));\n continue;\n }\n if (node.field === field && operators.has(node.operator)) {\n const value = conditionValueToNumber(node.value);\n if (value != null) values.push(value);\n }\n }\n return values;\n}\n\nconst MIN_PURCHASE_OPERATORS: ReadonlySet<RuleCondition[\"operator\"]> = new Set([\n \"gt\",\n \"gte\",\n \"between\",\n]);\nconst START_DATE_OPERATORS: ReadonlySet<RuleCondition[\"operator\"]> = new Set([\n \"gt\",\n \"gte\",\n]);\n\nexport function extractMinPurchaseAmount(\n conditions: RuleConditions\n): number | undefined {\n const values = collectConditionValues(\n conditions,\n \"purchase.amount\",\n MIN_PURCHASE_OPERATORS\n );\n return values.length > 0 ? Math.min(...values) : undefined;\n}\n\n// The business app encodes a campaign start as a `time.timestamp >= <unix s>`\n// rule condition; the endpoint returns it verbatim inside `conditions`.\nexport function extractStartDate(conditions: RuleConditions): Date | undefined {\n const values = collectConditionValues(\n conditions,\n \"time.timestamp\",\n START_DATE_OPERATORS\n );\n if (values.length === 0) return undefined;\n return new Date(Math.min(...values) * 1000);\n}\n","import type { EstimatedReward } from \"../types\";\n\ntype PercentageReward = Extract<EstimatedReward, { payoutType: \"percentage\" }>;\n\n// Reference order total used to turn a \"% of basket\" reward into a concrete\n// example amount. The campaign's minimum purchase overrides it when higher.\nconst REFERENCE_BASKET = 100;\n\nexport type RewardExample = {\n basket: number;\n reward: number;\n};\n\nfunction roundWithinRange(min: number, max: number): number {\n const rounded = Math.round((min + max) / 2 / 10) * 10;\n return Math.min(max, Math.max(min, rounded));\n}\n\nexport function pickFlatBasket(minPurchase: number | undefined): number {\n return Math.max(REFERENCE_BASKET, minPurchase ?? 0);\n}\n\n// Basket for a percentage tier: the reference when it sits inside the tier\n// range, a round number within the range otherwise, or the lower bound for an\n// open-ended (uncapped) top tier.\nexport function pickTierBasket(\n minValue: number,\n maxValue: number | undefined\n): number {\n if (maxValue == null) return minValue;\n if (minValue <= REFERENCE_BASKET && REFERENCE_BASKET <= maxValue) {\n return REFERENCE_BASKET;\n }\n return roundWithinRange(minValue, maxValue);\n}\n\nexport function buildPercentageExample(\n reward: PercentageReward,\n minPurchase: number | undefined\n): RewardExample | undefined {\n const basket = pickFlatBasket(minPurchase);\n if (basket <= 0) return undefined;\n let amount = (reward.percent / 100) * basket;\n if (reward.minAmount) amount = Math.max(amount, reward.minAmount.eurAmount);\n if (reward.maxAmount) amount = Math.min(amount, reward.maxAmount.eurAmount);\n return { basket, reward: amount };\n}\n\nexport function buildTierExample(\n percent: number,\n minValue: number,\n maxValue: number | undefined\n): RewardExample | undefined {\n const basket = pickTierBasket(minValue, maxValue);\n if (basket <= 0) return undefined;\n return { basket, reward: (percent / 100) * basket };\n}\n","import type { EstimatedReward, TokenAmountType } from \"../types\";\n\n/**\n * Comparable fiat value of a single reward, used to rank rewards against each\n * other.\n *\n * - `fixed` → the token amount in the requested currency\n * - `tiered` → the highest token amount across tiers\n * - `percentage` → the capped (`maxAmount`) value when present, otherwise `0`\n * (an uncapped percentage has no comparable fiat value)\n */\nexport function getRewardValue(\n reward: EstimatedReward,\n key: keyof TokenAmountType\n): number {\n switch (reward.payoutType) {\n case \"fixed\":\n return reward.amount[key];\n case \"percentage\":\n return reward.maxAmount?.[key] ?? 0;\n case \"tiered\":\n return reward.tiers.reduce(\n (max, tier) =>\n \"amount\" in tier ? Math.max(max, tier.amount[key]) : max,\n 0\n );\n }\n}\n\n/**\n * Highest percent a reward exposes: the flat `percentage` percent, or the\n * richest percent across `tiered` tiers, or `0` when it carries none.\n *\n * Shared by {@link getRewardRank} (to weight a percentage-only reward) and\n * `formatEstimatedReward` (to render it as `\"X %\"`), so the value the UI ranks\n * by and the value it prints stay derived from one tier traversal.\n */\nexport function maxRewardPercent(reward: EstimatedReward): number {\n if (reward.payoutType === \"percentage\") return reward.percent;\n if (reward.payoutType === \"tiered\") {\n return reward.tiers.reduce(\n (max, tier) =>\n \"percent\" in tier ? Math.max(max, tier.percent) : max,\n 0\n );\n }\n return 0;\n}\n\n// A reward with no money value (an uncapped percentage, or a percent-only\n// tier set) still renders as \"X %\", so it is worth surfacing. We give it a\n// positive ranking weight derived from its percent but scaled far below any\n// real-money reward, so the two invariants the UI relies on hold: (1) a reward\n// with real money always outranks a percentage-only reward, and (2) a\n// percentage-only reward still outranks a zero-value reward. Together they\n// guarantee the reward the ranking picks is always one we can display.\nconst PERCENT_ONLY_RANK_WEIGHT = 1e-6;\n\n/**\n * Ranking weight used to pick the single most attractive reward to surface.\n *\n * Mirrors {@link getRewardValue} (money value) but lifts a percentage-only\n * reward to a tiny positive weight instead of `0`, so it is never buried\n * behind a zero-value reward when choosing what to display.\n */\nexport function getRewardRank(\n reward: EstimatedReward,\n key: keyof TokenAmountType\n): number {\n const value = getRewardValue(reward, key);\n if (value > 0) return value;\n return maxRewardPercent(reward) * PERCENT_ONLY_RANK_WEIGHT;\n}\n","import type { Currency, EstimatedReward } from \"../types\";\nimport { formatAmount } from \"../utils/format/formatAmount\";\nimport { getCurrencyAmountKey } from \"../utils/format/getCurrencyAmountKey\";\nimport { getSupportedCurrency } from \"../utils/format/getSupportedCurrency\";\nimport { getRewardRank, getRewardValue, maxRewardPercent } from \"./value\";\n\n/**\n * Format an {@link EstimatedReward} into a human-readable string.\n *\n * - `fixed` → e.g. `\"5 €\"`\n * - `percentage` → e.g. `\"10 %\"`\n * - `tiered` → the richest tier: max token amount (e.g. `\"50 €\"`) or, when\n * tiers only carry percentages, the max percent (e.g. `\"10 %\"`)\n *\n * Percentages are always rendered as a `\"X %\"` string: the backend never sends\n * a reference basket, so the reward cannot be resolved to a concrete amount\n * here. Callers that need a worked example use the `example` helpers instead.\n */\nexport function formatEstimatedReward(\n reward: EstimatedReward,\n currency?: Currency\n): string {\n const supportedCurrency = getSupportedCurrency(currency);\n const key = getCurrencyAmountKey(supportedCurrency);\n\n switch (reward.payoutType) {\n case \"fixed\":\n return formatAmount(\n Math.round(reward.amount[key]),\n supportedCurrency\n );\n\n case \"percentage\":\n return `${reward.percent} %`;\n\n case \"tiered\": {\n const maxAmount = getRewardValue(reward, key);\n if (maxAmount > 0) {\n return formatAmount(Math.round(maxAmount), supportedCurrency);\n }\n const maxPercent = maxRewardPercent(reward);\n if (maxPercent > 0) {\n return `${maxPercent} %`;\n }\n return formatAmount(0, supportedCurrency);\n }\n }\n}\n\n/**\n * Format a reward for display, or return `undefined` when it is not worth\n * advertising. Callers rely on `undefined` to hide a badge / fall back to\n * other copy.\n *\n * A reward is hidden only when it carries no displayable value — a `fixed` or\n * `tiered` reward whose money value is `0` (e.g. an unknown token price). An\n * uncapped percentage always renders as `\"X %\"`, since the percent itself is\n * meaningful even without a money value.\n */\nexport function formatRewardOrHide(\n reward: EstimatedReward | undefined,\n currency?: Currency\n): string | undefined {\n if (!reward) return undefined;\n const key = getCurrencyAmountKey(getSupportedCurrency(currency));\n if (getRewardRank(reward, key) <= 0) return undefined;\n return formatEstimatedReward(reward, currency);\n}\n\n/**\n * Replace the `{REWARD}` placeholder in a text string with a reward value.\n * If no reward is provided, returns the text with `{REWARD}` stripped.\n */\nexport function applyRewardPlaceholder(\n text: string,\n reward: string | undefined\n): string {\n return reward\n ? text.replaceAll(\"{REWARD}\", reward)\n : text.replaceAll(\"{REWARD}\", \"\");\n}\n","import type { InteractionTypeKey } from \"../constants/interactionTypes\";\nimport type {\n Currency,\n EstimatedReward,\n MerchantReward,\n TokenAmountType,\n} from \"../types\";\nimport { formatAmount } from \"../utils/format/formatAmount\";\nimport { getCurrencyAmountKey } from \"../utils/format/getCurrencyAmountKey\";\nimport { getSupportedCurrency } from \"../utils/format/getSupportedCurrency\";\nimport { extractMinPurchaseAmount, extractStartDate } from \"./conditions\";\nimport { formatRewardOrHide } from \"./format\";\nimport { getRewardRank } from \"./value\";\n\n/** Reward side a surface cares about: the sharer (`referrer`) or the referred\n * user (`referee`). Drives both campaign ranking and which side is displayed. */\nexport type RewardAudience = \"referrer\" | \"referee\";\n\nexport type DisplayCampaign = {\n campaign: MerchantReward;\n status: \"live\" | \"upcoming\";\n startsAt?: Date;\n};\n\nexport type SelectDisplayCampaignOptions = {\n /** Reference \"now\" for expiry / start gating; defaults to the current time. */\n now?: Date;\n /** Currency the ranking is expressed in; defaults to EUR. */\n currency?: Currency;\n /** When set, only campaigns triggered by this interaction are considered. */\n targetInteraction?: InteractionTypeKey;\n /** Reward side to rank campaigns by; defaults to `\"referrer\"`. */\n audience?: RewardAudience;\n};\n\nfunction isExpired(campaign: MerchantReward, nowMs: number): boolean {\n return (\n campaign.expiresAt != null &&\n new Date(campaign.expiresAt).getTime() <= nowMs\n );\n}\n\nfunction hasStarted(campaign: MerchantReward, nowMs: number): boolean {\n const startsAt = extractStartDate(campaign.conditions);\n return startsAt == null || startsAt.getTime() <= nowMs;\n}\n\nfunction audienceReward(campaign: MerchantReward, audience: RewardAudience) {\n return audience === \"referee\" ? campaign.referee : campaign.referrer;\n}\n\nfunction campaignRank(\n campaign: MerchantReward,\n key: keyof TokenAmountType,\n audience: RewardAudience\n): number {\n const reward = audienceReward(campaign, audience);\n return reward ? getRewardRank(reward, key) : 0;\n}\n\n/**\n * Pick the single campaign a merchant surface should display.\n *\n * Filters out expired (and, when `targetInteraction` is set, non-matching)\n * campaigns, then prefers the highest-ranked *live* campaign — ranked by the\n * `audience` reward side in the requested currency. When none has started yet\n * it falls back to the soonest-starting upcoming campaign (the endpoint does\n * not gate on the start-date condition, so future-start campaigns come\n * through).\n */\nexport function selectDisplayCampaign(\n rewards: readonly MerchantReward[],\n options: SelectDisplayCampaignOptions = {}\n): DisplayCampaign | undefined {\n const nowMs = (options.now ?? new Date()).getTime();\n const audience = options.audience ?? \"referrer\";\n const key = getCurrencyAmountKey(getSupportedCurrency(options.currency));\n\n const matching = options.targetInteraction\n ? rewards.filter(\n (campaign) =>\n campaign.interactionTypeKey === options.targetInteraction\n )\n : rewards;\n const active = matching.filter((campaign) => !isExpired(campaign, nowMs));\n\n const live = active.filter((campaign) => hasStarted(campaign, nowMs));\n if (live.length > 0) {\n const best = live.reduce((a, b) =>\n campaignRank(b, key, audience) > campaignRank(a, key, audience)\n ? b\n : a\n );\n return { campaign: best, status: \"live\" };\n }\n\n const upcoming = active\n .map((campaign) => ({\n campaign,\n startsAt: extractStartDate(campaign.conditions),\n }))\n .filter(\n (entry): entry is { campaign: MerchantReward; startsAt: Date } =>\n entry.startsAt != null\n );\n if (upcoming.length === 0) return undefined;\n\n const soonest = upcoming.reduce((a, b) =>\n b.startsAt.getTime() < a.startsAt.getTime() ? b : a\n );\n return {\n campaign: soonest.campaign,\n status: \"upcoming\",\n startsAt: soonest.startsAt,\n };\n}\n\n/**\n * The single reward a merchant surface should display: its formatted string\n * plus the `payoutType` of the underlying reward, so surfaces can adapt their\n * presentation (e.g. hide percentage rewards, prefix tiered ones with \"Up to\").\n */\nexport type BestReward = {\n /** Display-ready reward string (e.g. `\"5 €\"`, `\"10 %\"`). */\n formatted: string;\n /** Payout type of the selected reward. */\n payoutType: EstimatedReward[\"payoutType\"];\n /**\n * Minimum purchase amount gating the reward, formatted with the requested\n * currency (e.g. `\"10 €\"`), or `undefined` when the campaign sets no\n * minimum.\n */\n minPurchaseAmount?: string;\n /**\n * Whole-day lockup applied before the reward settles, or `undefined` when\n * the campaign has no lockup.\n */\n lockupDurationDays?: number;\n /**\n * Raw referrer/referee rewards of the selected campaign, surfaced so\n * consumers can render the full per-audience breakdown (tier rows,\n * percentage examples) rather than only the headline number.\n */\n referrerReward?: EstimatedReward;\n refereeReward?: EstimatedReward;\n /**\n * Raw minimum purchase value (unformatted), used to build percentage\n * worked-examples consistent with the campaign's gating.\n */\n minPurchaseValue?: number;\n};\n\n/**\n * Pick the best campaign for `options` and resolve its `audience`-side reward\n * to a formatted string plus its `payoutType`, or `undefined` when there is\n * nothing worth showing.\n *\n * Single entry point shared by every \"headline reward\" surface (share button,\n * wallet modal, sharing/install screens) so they all show the same number for\n * a given merchant and can branch on the payout type.\n */\nexport function selectBestReward(\n rewards: readonly MerchantReward[],\n options: SelectDisplayCampaignOptions = {}\n): BestReward | undefined {\n const selected = selectDisplayCampaign(rewards, options);\n if (!selected) return undefined;\n const reward = audienceReward(\n selected.campaign,\n options.audience ?? \"referrer\"\n );\n if (!reward) return undefined;\n const formatted = formatRewardOrHide(reward, options.currency);\n if (!formatted) return undefined;\n\n const minPurchase = extractMinPurchaseAmount(selected.campaign.conditions);\n const minPurchaseAmount =\n minPurchase != null\n ? formatAmount(minPurchase, options.currency)\n : undefined;\n\n const lockupSeconds = selected.campaign.defaultLockupSeconds;\n const lockupDurationDays =\n lockupSeconds && lockupSeconds > 0\n ? Math.round(lockupSeconds / 86_400)\n : undefined;\n\n return {\n formatted,\n payoutType: reward.payoutType,\n minPurchaseAmount,\n lockupDurationDays,\n referrerReward: selected.campaign.referrer,\n refereeReward: selected.campaign.referee,\n minPurchaseValue: minPurchase ?? undefined,\n };\n}\n\n/**\n * Headline reward string for a merchant: picks the best campaign for `options`\n * and formats its `audience`-side reward, or returns `undefined` when there is\n * nothing worth showing.\n *\n * Thin wrapper over {@link selectBestReward} for callers that only need the\n * formatted string.\n */\nexport function formatBestReward(\n rewards: readonly MerchantReward[],\n options: SelectDisplayCampaignOptions = {}\n): string | undefined {\n return selectBestReward(rewards, options)?.formatted;\n}\n"],"mappings":"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"}
|
package/dist/rewards.d.cts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { a as MerchantReward, c as RuleConditions, d as InteractionTypeKey, f as Currency, r as EstimatedReward, u as TokenAmountType } from "./merchantInformation-
|
|
2
|
-
|
|
1
|
+
import { a as MerchantReward, c as RuleConditions, d as InteractionTypeKey, f as Currency, r as EstimatedReward, u as TokenAmountType } from "./merchantInformation-BFdhF3De.cjs";
|
|
3
2
|
//#region src/rewards/conditions.d.ts
|
|
4
3
|
declare function extractMinPurchaseAmount(conditions: RuleConditions): number | undefined;
|
|
5
4
|
declare function extractStartDate(conditions: RuleConditions): Date | undefined;
|
|
@@ -58,9 +57,13 @@ type DisplayCampaign = {
|
|
|
58
57
|
startsAt?: Date;
|
|
59
58
|
};
|
|
60
59
|
type SelectDisplayCampaignOptions = {
|
|
61
|
-
/** Reference "now" for expiry / start gating; defaults to the current time. */
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
/** Reference "now" for expiry / start gating; defaults to the current time. */
|
|
61
|
+
now?: Date;
|
|
62
|
+
/** Currency the ranking is expressed in; defaults to EUR. */
|
|
63
|
+
currency?: Currency;
|
|
64
|
+
/** When set, only campaigns triggered by this interaction are considered. */
|
|
65
|
+
targetInteraction?: InteractionTypeKey;
|
|
66
|
+
/** Reward side to rank campaigns by; defaults to `"referrer"`. */
|
|
64
67
|
audience?: RewardAudience;
|
|
65
68
|
};
|
|
66
69
|
/**
|
|
@@ -80,7 +83,9 @@ declare function selectDisplayCampaign(rewards: readonly MerchantReward[], optio
|
|
|
80
83
|
* presentation (e.g. hide percentage rewards, prefix tiered ones with "Up to").
|
|
81
84
|
*/
|
|
82
85
|
type BestReward = {
|
|
83
|
-
/** Display-ready reward string (e.g. `"5 €"`, `"10 %"`). */
|
|
86
|
+
/** Display-ready reward string (e.g. `"5 €"`, `"10 %"`). */
|
|
87
|
+
formatted: string;
|
|
88
|
+
/** Payout type of the selected reward. */
|
|
84
89
|
payoutType: EstimatedReward["payoutType"];
|
|
85
90
|
/**
|
|
86
91
|
* Minimum purchase amount gating the reward, formatted with the requested
|
package/dist/rewards.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { a as MerchantReward, c as RuleConditions, d as InteractionTypeKey, f as Currency, r as EstimatedReward, u as TokenAmountType } from "./merchantInformation
|
|
2
|
-
|
|
1
|
+
import { a as MerchantReward, c as RuleConditions, d as InteractionTypeKey, f as Currency, r as EstimatedReward, u as TokenAmountType } from "./merchantInformation-BFdhF3De.js";
|
|
3
2
|
//#region src/rewards/conditions.d.ts
|
|
4
3
|
declare function extractMinPurchaseAmount(conditions: RuleConditions): number | undefined;
|
|
5
4
|
declare function extractStartDate(conditions: RuleConditions): Date | undefined;
|
|
@@ -58,9 +57,13 @@ type DisplayCampaign = {
|
|
|
58
57
|
startsAt?: Date;
|
|
59
58
|
};
|
|
60
59
|
type SelectDisplayCampaignOptions = {
|
|
61
|
-
/** Reference "now" for expiry / start gating; defaults to the current time. */
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
/** Reference "now" for expiry / start gating; defaults to the current time. */
|
|
61
|
+
now?: Date;
|
|
62
|
+
/** Currency the ranking is expressed in; defaults to EUR. */
|
|
63
|
+
currency?: Currency;
|
|
64
|
+
/** When set, only campaigns triggered by this interaction are considered. */
|
|
65
|
+
targetInteraction?: InteractionTypeKey;
|
|
66
|
+
/** Reward side to rank campaigns by; defaults to `"referrer"`. */
|
|
64
67
|
audience?: RewardAudience;
|
|
65
68
|
};
|
|
66
69
|
/**
|
|
@@ -80,7 +83,9 @@ declare function selectDisplayCampaign(rewards: readonly MerchantReward[], optio
|
|
|
80
83
|
* presentation (e.g. hide percentage rewards, prefix tiered ones with "Up to").
|
|
81
84
|
*/
|
|
82
85
|
type BestReward = {
|
|
83
|
-
/** Display-ready reward string (e.g. `"5 €"`, `"10 %"`). */
|
|
86
|
+
/** Display-ready reward string (e.g. `"5 €"`, `"10 %"`). */
|
|
87
|
+
formatted: string;
|
|
88
|
+
/** Payout type of the selected reward. */
|
|
84
89
|
payoutType: EstimatedReward["payoutType"];
|
|
85
90
|
/**
|
|
86
91
|
* Minimum purchase amount gating the reward, formatted with the requested
|
package/dist/rewards.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{n as e,r as t,t as n}from"./getCurrencyAmountKey-
|
|
1
|
+
import{n as e,r as t,t as n}from"./getCurrencyAmountKey-C-1W-BRg.js";function r(e){return Array.isArray(e)?e:e.conditions}function i(e){if(typeof e==`number`)return e;if(typeof e==`string`&&e.trim()!==``){let t=Number(e);return Number.isFinite(t)?t:void 0}}function a(e,t,n){let o=[];for(let s of r(e)){if(`logic`in s){o.push(...a(s,t,n));continue}if(s.field===t&&n.has(s.operator)){let e=i(s.value);e!=null&&o.push(e)}}return o}const o=new Set([`gt`,`gte`,`between`]),s=new Set([`gt`,`gte`]);function c(e){let t=a(e,`purchase.amount`,o);return t.length>0?Math.min(...t):void 0}function l(e){let t=a(e,`time.timestamp`,s);if(t.length!==0)return new Date(Math.min(...t)*1e3)}function u(e,t){let n=Math.round((e+t)/2/10)*10;return Math.min(t,Math.max(e,n))}function d(e){return Math.max(100,e??0)}function f(e,t){return t==null?e:e<=100&&100<=t?100:u(e,t)}function p(e,t){let n=d(t);if(n<=0)return;let r=e.percent/100*n;return e.minAmount&&(r=Math.max(r,e.minAmount.eurAmount)),e.maxAmount&&(r=Math.min(r,e.maxAmount.eurAmount)),{basket:n,reward:r}}function m(e,t,n){let r=f(t,n);if(!(r<=0))return{basket:r,reward:e/100*r}}function h(e,t){switch(e.payoutType){case`fixed`:return e.amount[t];case`percentage`:return e.maxAmount?.[t]??0;case`tiered`:return e.tiers.reduce((e,n)=>`amount`in n?Math.max(e,n.amount[t]):e,0)}}function g(e){return e.payoutType===`percentage`?e.percent:e.payoutType===`tiered`?e.tiers.reduce((e,t)=>`percent`in t?Math.max(e,t.percent):e,0):0}function _(e,t){let n=h(e,t);return n>0?n:g(e)*1e-6}function v(r,i){let a=t(i),o=n(a);switch(r.payoutType){case`fixed`:return e(Math.round(r.amount[o]),a);case`percentage`:return`${r.percent} %`;case`tiered`:{let t=h(r,o);if(t>0)return e(Math.round(t),a);let n=g(r);return n>0?`${n} %`:e(0,a)}}}function y(e,r){if(e&&!(_(e,n(t(r)))<=0))return v(e,r)}function b(e,t){return t?e.replaceAll(`{REWARD}`,t):e.replaceAll(`{REWARD}`,``)}function x(e,t){return e.expiresAt!=null&&new Date(e.expiresAt).getTime()<=t}function S(e,t){let n=l(e.conditions);return n==null||n.getTime()<=t}function C(e,t){return t===`referee`?e.referee:e.referrer}function w(e,t,n){let r=C(e,n);return r?_(r,t):0}function T(e,r={}){let i=(r.now??new Date).getTime(),a=r.audience??`referrer`,o=n(t(r.currency)),s=(r.targetInteraction?e.filter(e=>e.interactionTypeKey===r.targetInteraction):e).filter(e=>!x(e,i)),c=s.filter(e=>S(e,i));if(c.length>0)return{campaign:c.reduce((e,t)=>w(t,o,a)>w(e,o,a)?t:e),status:`live`};let u=s.map(e=>({campaign:e,startsAt:l(e.conditions)})).filter(e=>e.startsAt!=null);if(u.length===0)return;let d=u.reduce((e,t)=>t.startsAt.getTime()<e.startsAt.getTime()?t:e);return{campaign:d.campaign,status:`upcoming`,startsAt:d.startsAt}}function E(t,n={}){let r=T(t,n);if(!r)return;let i=C(r.campaign,n.audience??`referrer`);if(!i)return;let a=y(i,n.currency);if(!a)return;let o=c(r.campaign.conditions),s=o==null?void 0:e(o,n.currency),l=r.campaign.defaultLockupSeconds,u=l&&l>0?Math.round(l/86400):void 0;return{formatted:a,payoutType:i.payoutType,minPurchaseAmount:s,lockupDurationDays:u,referrerReward:r.campaign.referrer,refereeReward:r.campaign.referee,minPurchaseValue:o??void 0}}function D(e,t={}){return E(e,t)?.formatted}export{b as applyRewardPlaceholder,p as buildPercentageExample,m as buildTierExample,c as extractMinPurchaseAmount,l as extractStartDate,D as formatBestReward,v as formatEstimatedReward,y as formatRewardOrHide,h as getRewardValue,d as pickFlatBasket,f as pickTierBasket,E as selectBestReward,T as selectDisplayCampaign};
|
|
2
2
|
//# sourceMappingURL=rewards.js.map
|
package/dist/rewards.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rewards.js","names":[],"sources":["../src/rewards/conditions.ts","../src/rewards/example.ts","../src/rewards/value.ts","../src/rewards/format.ts","../src/rewards/select.ts"],"sourcesContent":["import type { ConditionGroup, RuleCondition, RuleConditions } from \"../types\";\n\ntype ConditionNode = RuleCondition | ConditionGroup;\n\nfunction conditionNodes(conditions: RuleConditions): ConditionNode[] {\n return Array.isArray(conditions) ? conditions : conditions.conditions;\n}\n\nfunction conditionValueToNumber(\n value: RuleCondition[\"value\"]\n): number | undefined {\n if (typeof value === \"number\") return value;\n if (typeof value === \"string\" && value.trim() !== \"\") {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n }\n return undefined;\n}\n\n// Conditions arrive as a flat list or recursive groups; collect every numeric\n// value whose field + operator match, descending into nested groups.\nfunction collectConditionValues(\n conditions: RuleConditions,\n field: string,\n operators: ReadonlySet<RuleCondition[\"operator\"]>\n): number[] {\n const values: number[] = [];\n for (const node of conditionNodes(conditions)) {\n if (\"logic\" in node) {\n values.push(...collectConditionValues(node, field, operators));\n continue;\n }\n if (node.field === field && operators.has(node.operator)) {\n const value = conditionValueToNumber(node.value);\n if (value != null) values.push(value);\n }\n }\n return values;\n}\n\nconst MIN_PURCHASE_OPERATORS: ReadonlySet<RuleCondition[\"operator\"]> = new Set([\n \"gt\",\n \"gte\",\n \"between\",\n]);\nconst START_DATE_OPERATORS: ReadonlySet<RuleCondition[\"operator\"]> = new Set([\n \"gt\",\n \"gte\",\n]);\n\nexport function extractMinPurchaseAmount(\n conditions: RuleConditions\n): number | undefined {\n const values = collectConditionValues(\n conditions,\n \"purchase.amount\",\n MIN_PURCHASE_OPERATORS\n );\n return values.length > 0 ? Math.min(...values) : undefined;\n}\n\n// The business app encodes a campaign start as a `time.timestamp >= <unix s>`\n// rule condition; the endpoint returns it verbatim inside `conditions`.\nexport function extractStartDate(conditions: RuleConditions): Date | undefined {\n const values = collectConditionValues(\n conditions,\n \"time.timestamp\",\n START_DATE_OPERATORS\n );\n if (values.length === 0) return undefined;\n return new Date(Math.min(...values) * 1000);\n}\n","import type { EstimatedReward } from \"../types\";\n\ntype PercentageReward = Extract<EstimatedReward, { payoutType: \"percentage\" }>;\n\n// Reference order total used to turn a \"% of basket\" reward into a concrete\n// example amount. The campaign's minimum purchase overrides it when higher.\nconst REFERENCE_BASKET = 100;\n\nexport type RewardExample = {\n basket: number;\n reward: number;\n};\n\nfunction roundWithinRange(min: number, max: number): number {\n const rounded = Math.round((min + max) / 2 / 10) * 10;\n return Math.min(max, Math.max(min, rounded));\n}\n\nexport function pickFlatBasket(minPurchase: number | undefined): number {\n return Math.max(REFERENCE_BASKET, minPurchase ?? 0);\n}\n\n// Basket for a percentage tier: the reference when it sits inside the tier\n// range, a round number within the range otherwise, or the lower bound for an\n// open-ended (uncapped) top tier.\nexport function pickTierBasket(\n minValue: number,\n maxValue: number | undefined\n): number {\n if (maxValue == null) return minValue;\n if (minValue <= REFERENCE_BASKET && REFERENCE_BASKET <= maxValue) {\n return REFERENCE_BASKET;\n }\n return roundWithinRange(minValue, maxValue);\n}\n\nexport function buildPercentageExample(\n reward: PercentageReward,\n minPurchase: number | undefined\n): RewardExample | undefined {\n const basket = pickFlatBasket(minPurchase);\n if (basket <= 0) return undefined;\n let amount = (reward.percent / 100) * basket;\n if (reward.minAmount) amount = Math.max(amount, reward.minAmount.eurAmount);\n if (reward.maxAmount) amount = Math.min(amount, reward.maxAmount.eurAmount);\n return { basket, reward: amount };\n}\n\nexport function buildTierExample(\n percent: number,\n minValue: number,\n maxValue: number | undefined\n): RewardExample | undefined {\n const basket = pickTierBasket(minValue, maxValue);\n if (basket <= 0) return undefined;\n return { basket, reward: (percent / 100) * basket };\n}\n","import type { EstimatedReward, TokenAmountType } from \"../types\";\n\n/**\n * Comparable fiat value of a single reward, used to rank rewards against each\n * other.\n *\n * - `fixed` → the token amount in the requested currency\n * - `tiered` → the highest token amount across tiers\n * - `percentage` → the capped (`maxAmount`) value when present, otherwise `0`\n * (an uncapped percentage has no comparable fiat value)\n */\nexport function getRewardValue(\n reward: EstimatedReward,\n key: keyof TokenAmountType\n): number {\n switch (reward.payoutType) {\n case \"fixed\":\n return reward.amount[key];\n case \"percentage\":\n return reward.maxAmount?.[key] ?? 0;\n case \"tiered\":\n return reward.tiers.reduce(\n (max, tier) =>\n \"amount\" in tier ? Math.max(max, tier.amount[key]) : max,\n 0\n );\n }\n}\n\n/**\n * Highest percent a reward exposes: the flat `percentage` percent, or the\n * richest percent across `tiered` tiers, or `0` when it carries none.\n *\n * Shared by {@link getRewardRank} (to weight a percentage-only reward) and\n * `formatEstimatedReward` (to render it as `\"X %\"`), so the value the UI ranks\n * by and the value it prints stay derived from one tier traversal.\n */\nexport function maxRewardPercent(reward: EstimatedReward): number {\n if (reward.payoutType === \"percentage\") return reward.percent;\n if (reward.payoutType === \"tiered\") {\n return reward.tiers.reduce(\n (max, tier) =>\n \"percent\" in tier ? Math.max(max, tier.percent) : max,\n 0\n );\n }\n return 0;\n}\n\n// A reward with no money value (an uncapped percentage, or a percent-only\n// tier set) still renders as \"X %\", so it is worth surfacing. We give it a\n// positive ranking weight derived from its percent but scaled far below any\n// real-money reward, so the two invariants the UI relies on hold: (1) a reward\n// with real money always outranks a percentage-only reward, and (2) a\n// percentage-only reward still outranks a zero-value reward. Together they\n// guarantee the reward the ranking picks is always one we can display.\nconst PERCENT_ONLY_RANK_WEIGHT = 1e-6;\n\n/**\n * Ranking weight used to pick the single most attractive reward to surface.\n *\n * Mirrors {@link getRewardValue} (money value) but lifts a percentage-only\n * reward to a tiny positive weight instead of `0`, so it is never buried\n * behind a zero-value reward when choosing what to display.\n */\nexport function getRewardRank(\n reward: EstimatedReward,\n key: keyof TokenAmountType\n): number {\n const value = getRewardValue(reward, key);\n if (value > 0) return value;\n return maxRewardPercent(reward) * PERCENT_ONLY_RANK_WEIGHT;\n}\n","import type { Currency, EstimatedReward } from \"../types\";\nimport { formatAmount } from \"../utils/format/formatAmount\";\nimport { getCurrencyAmountKey } from \"../utils/format/getCurrencyAmountKey\";\nimport { getSupportedCurrency } from \"../utils/format/getSupportedCurrency\";\nimport { getRewardRank, getRewardValue, maxRewardPercent } from \"./value\";\n\n/**\n * Format an {@link EstimatedReward} into a human-readable string.\n *\n * - `fixed` → e.g. `\"5 €\"`\n * - `percentage` → e.g. `\"10 %\"`\n * - `tiered` → the richest tier: max token amount (e.g. `\"50 €\"`) or, when\n * tiers only carry percentages, the max percent (e.g. `\"10 %\"`)\n *\n * Percentages are always rendered as a `\"X %\"` string: the backend never sends\n * a reference basket, so the reward cannot be resolved to a concrete amount\n * here. Callers that need a worked example use the `example` helpers instead.\n */\nexport function formatEstimatedReward(\n reward: EstimatedReward,\n currency?: Currency\n): string {\n const supportedCurrency = getSupportedCurrency(currency);\n const key = getCurrencyAmountKey(supportedCurrency);\n\n switch (reward.payoutType) {\n case \"fixed\":\n return formatAmount(\n Math.round(reward.amount[key]),\n supportedCurrency\n );\n\n case \"percentage\":\n return `${reward.percent} %`;\n\n case \"tiered\": {\n const maxAmount = getRewardValue(reward, key);\n if (maxAmount > 0) {\n return formatAmount(Math.round(maxAmount), supportedCurrency);\n }\n const maxPercent = maxRewardPercent(reward);\n if (maxPercent > 0) {\n return `${maxPercent} %`;\n }\n return formatAmount(0, supportedCurrency);\n }\n }\n}\n\n/**\n * Format a reward for display, or return `undefined` when it is not worth\n * advertising. Callers rely on `undefined` to hide a badge / fall back to\n * other copy.\n *\n * A reward is hidden only when it carries no displayable value — a `fixed` or\n * `tiered` reward whose money value is `0` (e.g. an unknown token price). An\n * uncapped percentage always renders as `\"X %\"`, since the percent itself is\n * meaningful even without a money value.\n */\nexport function formatRewardOrHide(\n reward: EstimatedReward | undefined,\n currency?: Currency\n): string | undefined {\n if (!reward) return undefined;\n const key = getCurrencyAmountKey(getSupportedCurrency(currency));\n if (getRewardRank(reward, key) <= 0) return undefined;\n return formatEstimatedReward(reward, currency);\n}\n\n/**\n * Replace the `{REWARD}` placeholder in a text string with a reward value.\n * If no reward is provided, returns the text with `{REWARD}` stripped.\n */\nexport function applyRewardPlaceholder(\n text: string,\n reward: string | undefined\n): string {\n return reward\n ? text.replaceAll(\"{REWARD}\", reward)\n : text.replaceAll(\"{REWARD}\", \"\");\n}\n","import type { InteractionTypeKey } from \"../constants/interactionTypes\";\nimport type {\n Currency,\n EstimatedReward,\n MerchantReward,\n TokenAmountType,\n} from \"../types\";\nimport { formatAmount } from \"../utils/format/formatAmount\";\nimport { getCurrencyAmountKey } from \"../utils/format/getCurrencyAmountKey\";\nimport { getSupportedCurrency } from \"../utils/format/getSupportedCurrency\";\nimport { extractMinPurchaseAmount, extractStartDate } from \"./conditions\";\nimport { formatRewardOrHide } from \"./format\";\nimport { getRewardRank } from \"./value\";\n\n/** Reward side a surface cares about: the sharer (`referrer`) or the referred\n * user (`referee`). Drives both campaign ranking and which side is displayed. */\nexport type RewardAudience = \"referrer\" | \"referee\";\n\nexport type DisplayCampaign = {\n campaign: MerchantReward;\n status: \"live\" | \"upcoming\";\n startsAt?: Date;\n};\n\nexport type SelectDisplayCampaignOptions = {\n /** Reference \"now\" for expiry / start gating; defaults to the current time. */\n now?: Date;\n /** Currency the ranking is expressed in; defaults to EUR. */\n currency?: Currency;\n /** When set, only campaigns triggered by this interaction are considered. */\n targetInteraction?: InteractionTypeKey;\n /** Reward side to rank campaigns by; defaults to `\"referrer\"`. */\n audience?: RewardAudience;\n};\n\nfunction isExpired(campaign: MerchantReward, nowMs: number): boolean {\n return (\n campaign.expiresAt != null &&\n new Date(campaign.expiresAt).getTime() <= nowMs\n );\n}\n\nfunction hasStarted(campaign: MerchantReward, nowMs: number): boolean {\n const startsAt = extractStartDate(campaign.conditions);\n return startsAt == null || startsAt.getTime() <= nowMs;\n}\n\nfunction audienceReward(campaign: MerchantReward, audience: RewardAudience) {\n return audience === \"referee\" ? campaign.referee : campaign.referrer;\n}\n\nfunction campaignRank(\n campaign: MerchantReward,\n key: keyof TokenAmountType,\n audience: RewardAudience\n): number {\n const reward = audienceReward(campaign, audience);\n return reward ? getRewardRank(reward, key) : 0;\n}\n\n/**\n * Pick the single campaign a merchant surface should display.\n *\n * Filters out expired (and, when `targetInteraction` is set, non-matching)\n * campaigns, then prefers the highest-ranked *live* campaign — ranked by the\n * `audience` reward side in the requested currency. When none has started yet\n * it falls back to the soonest-starting upcoming campaign (the endpoint does\n * not gate on the start-date condition, so future-start campaigns come\n * through).\n */\nexport function selectDisplayCampaign(\n rewards: readonly MerchantReward[],\n options: SelectDisplayCampaignOptions = {}\n): DisplayCampaign | undefined {\n const nowMs = (options.now ?? new Date()).getTime();\n const audience = options.audience ?? \"referrer\";\n const key = getCurrencyAmountKey(getSupportedCurrency(options.currency));\n\n const matching = options.targetInteraction\n ? rewards.filter(\n (campaign) =>\n campaign.interactionTypeKey === options.targetInteraction\n )\n : rewards;\n const active = matching.filter((campaign) => !isExpired(campaign, nowMs));\n\n const live = active.filter((campaign) => hasStarted(campaign, nowMs));\n if (live.length > 0) {\n const best = live.reduce((a, b) =>\n campaignRank(b, key, audience) > campaignRank(a, key, audience)\n ? b\n : a\n );\n return { campaign: best, status: \"live\" };\n }\n\n const upcoming = active\n .map((campaign) => ({\n campaign,\n startsAt: extractStartDate(campaign.conditions),\n }))\n .filter(\n (entry): entry is { campaign: MerchantReward; startsAt: Date } =>\n entry.startsAt != null\n );\n if (upcoming.length === 0) return undefined;\n\n const soonest = upcoming.reduce((a, b) =>\n b.startsAt.getTime() < a.startsAt.getTime() ? b : a\n );\n return {\n campaign: soonest.campaign,\n status: \"upcoming\",\n startsAt: soonest.startsAt,\n };\n}\n\n/**\n * The single reward a merchant surface should display: its formatted string\n * plus the `payoutType` of the underlying reward, so surfaces can adapt their\n * presentation (e.g. hide percentage rewards, prefix tiered ones with \"Up to\").\n */\nexport type BestReward = {\n /** Display-ready reward string (e.g. `\"5 €\"`, `\"10 %\"`). */\n formatted: string;\n /** Payout type of the selected reward. */\n payoutType: EstimatedReward[\"payoutType\"];\n /**\n * Minimum purchase amount gating the reward, formatted with the requested\n * currency (e.g. `\"10 €\"`), or `undefined` when the campaign sets no\n * minimum.\n */\n minPurchaseAmount?: string;\n /**\n * Whole-day lockup applied before the reward settles, or `undefined` when\n * the campaign has no lockup.\n */\n lockupDurationDays?: number;\n /**\n * Raw referrer/referee rewards of the selected campaign, surfaced so\n * consumers can render the full per-audience breakdown (tier rows,\n * percentage examples) rather than only the headline number.\n */\n referrerReward?: EstimatedReward;\n refereeReward?: EstimatedReward;\n /**\n * Raw minimum purchase value (unformatted), used to build percentage\n * worked-examples consistent with the campaign's gating.\n */\n minPurchaseValue?: number;\n};\n\n/**\n * Pick the best campaign for `options` and resolve its `audience`-side reward\n * to a formatted string plus its `payoutType`, or `undefined` when there is\n * nothing worth showing.\n *\n * Single entry point shared by every \"headline reward\" surface (share button,\n * wallet modal, sharing/install screens) so they all show the same number for\n * a given merchant and can branch on the payout type.\n */\nexport function selectBestReward(\n rewards: readonly MerchantReward[],\n options: SelectDisplayCampaignOptions = {}\n): BestReward | undefined {\n const selected = selectDisplayCampaign(rewards, options);\n if (!selected) return undefined;\n const reward = audienceReward(\n selected.campaign,\n options.audience ?? \"referrer\"\n );\n if (!reward) return undefined;\n const formatted = formatRewardOrHide(reward, options.currency);\n if (!formatted) return undefined;\n\n const minPurchase = extractMinPurchaseAmount(selected.campaign.conditions);\n const minPurchaseAmount =\n minPurchase != null\n ? formatAmount(minPurchase, options.currency)\n : undefined;\n\n const lockupSeconds = selected.campaign.defaultLockupSeconds;\n const lockupDurationDays =\n lockupSeconds && lockupSeconds > 0\n ? Math.round(lockupSeconds / 86_400)\n : undefined;\n\n return {\n formatted,\n payoutType: reward.payoutType,\n minPurchaseAmount,\n lockupDurationDays,\n referrerReward: selected.campaign.referrer,\n refereeReward: selected.campaign.referee,\n minPurchaseValue: minPurchase ?? undefined,\n };\n}\n\n/**\n * Headline reward string for a merchant: picks the best campaign for `options`\n * and formats its `audience`-side reward, or returns `undefined` when there is\n * nothing worth showing.\n *\n * Thin wrapper over {@link selectBestReward} for callers that only need the\n * formatted string.\n */\nexport function formatBestReward(\n rewards: readonly MerchantReward[],\n options: SelectDisplayCampaignOptions = {}\n): string | undefined {\n return selectBestReward(rewards, options)?.formatted;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rewards.js","names":[],"sources":["../src/rewards/conditions.ts","../src/rewards/example.ts","../src/rewards/value.ts","../src/rewards/format.ts","../src/rewards/select.ts"],"sourcesContent":["import type { ConditionGroup, RuleCondition, RuleConditions } from \"../types\";\n\ntype ConditionNode = RuleCondition | ConditionGroup;\n\nfunction conditionNodes(conditions: RuleConditions): ConditionNode[] {\n return Array.isArray(conditions) ? conditions : conditions.conditions;\n}\n\nfunction conditionValueToNumber(\n value: RuleCondition[\"value\"]\n): number | undefined {\n if (typeof value === \"number\") return value;\n if (typeof value === \"string\" && value.trim() !== \"\") {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n }\n return undefined;\n}\n\n// Conditions arrive as a flat list or recursive groups; collect every numeric\n// value whose field + operator match, descending into nested groups.\nfunction collectConditionValues(\n conditions: RuleConditions,\n field: string,\n operators: ReadonlySet<RuleCondition[\"operator\"]>\n): number[] {\n const values: number[] = [];\n for (const node of conditionNodes(conditions)) {\n if (\"logic\" in node) {\n values.push(...collectConditionValues(node, field, operators));\n continue;\n }\n if (node.field === field && operators.has(node.operator)) {\n const value = conditionValueToNumber(node.value);\n if (value != null) values.push(value);\n }\n }\n return values;\n}\n\nconst MIN_PURCHASE_OPERATORS: ReadonlySet<RuleCondition[\"operator\"]> = new Set([\n \"gt\",\n \"gte\",\n \"between\",\n]);\nconst START_DATE_OPERATORS: ReadonlySet<RuleCondition[\"operator\"]> = new Set([\n \"gt\",\n \"gte\",\n]);\n\nexport function extractMinPurchaseAmount(\n conditions: RuleConditions\n): number | undefined {\n const values = collectConditionValues(\n conditions,\n \"purchase.amount\",\n MIN_PURCHASE_OPERATORS\n );\n return values.length > 0 ? Math.min(...values) : undefined;\n}\n\n// The business app encodes a campaign start as a `time.timestamp >= <unix s>`\n// rule condition; the endpoint returns it verbatim inside `conditions`.\nexport function extractStartDate(conditions: RuleConditions): Date | undefined {\n const values = collectConditionValues(\n conditions,\n \"time.timestamp\",\n START_DATE_OPERATORS\n );\n if (values.length === 0) return undefined;\n return new Date(Math.min(...values) * 1000);\n}\n","import type { EstimatedReward } from \"../types\";\n\ntype PercentageReward = Extract<EstimatedReward, { payoutType: \"percentage\" }>;\n\n// Reference order total used to turn a \"% of basket\" reward into a concrete\n// example amount. The campaign's minimum purchase overrides it when higher.\nconst REFERENCE_BASKET = 100;\n\nexport type RewardExample = {\n basket: number;\n reward: number;\n};\n\nfunction roundWithinRange(min: number, max: number): number {\n const rounded = Math.round((min + max) / 2 / 10) * 10;\n return Math.min(max, Math.max(min, rounded));\n}\n\nexport function pickFlatBasket(minPurchase: number | undefined): number {\n return Math.max(REFERENCE_BASKET, minPurchase ?? 0);\n}\n\n// Basket for a percentage tier: the reference when it sits inside the tier\n// range, a round number within the range otherwise, or the lower bound for an\n// open-ended (uncapped) top tier.\nexport function pickTierBasket(\n minValue: number,\n maxValue: number | undefined\n): number {\n if (maxValue == null) return minValue;\n if (minValue <= REFERENCE_BASKET && REFERENCE_BASKET <= maxValue) {\n return REFERENCE_BASKET;\n }\n return roundWithinRange(minValue, maxValue);\n}\n\nexport function buildPercentageExample(\n reward: PercentageReward,\n minPurchase: number | undefined\n): RewardExample | undefined {\n const basket = pickFlatBasket(minPurchase);\n if (basket <= 0) return undefined;\n let amount = (reward.percent / 100) * basket;\n if (reward.minAmount) amount = Math.max(amount, reward.minAmount.eurAmount);\n if (reward.maxAmount) amount = Math.min(amount, reward.maxAmount.eurAmount);\n return { basket, reward: amount };\n}\n\nexport function buildTierExample(\n percent: number,\n minValue: number,\n maxValue: number | undefined\n): RewardExample | undefined {\n const basket = pickTierBasket(minValue, maxValue);\n if (basket <= 0) return undefined;\n return { basket, reward: (percent / 100) * basket };\n}\n","import type { EstimatedReward, TokenAmountType } from \"../types\";\n\n/**\n * Comparable fiat value of a single reward, used to rank rewards against each\n * other.\n *\n * - `fixed` → the token amount in the requested currency\n * - `tiered` → the highest token amount across tiers\n * - `percentage` → the capped (`maxAmount`) value when present, otherwise `0`\n * (an uncapped percentage has no comparable fiat value)\n */\nexport function getRewardValue(\n reward: EstimatedReward,\n key: keyof TokenAmountType\n): number {\n switch (reward.payoutType) {\n case \"fixed\":\n return reward.amount[key];\n case \"percentage\":\n return reward.maxAmount?.[key] ?? 0;\n case \"tiered\":\n return reward.tiers.reduce(\n (max, tier) =>\n \"amount\" in tier ? Math.max(max, tier.amount[key]) : max,\n 0\n );\n }\n}\n\n/**\n * Highest percent a reward exposes: the flat `percentage` percent, or the\n * richest percent across `tiered` tiers, or `0` when it carries none.\n *\n * Shared by {@link getRewardRank} (to weight a percentage-only reward) and\n * `formatEstimatedReward` (to render it as `\"X %\"`), so the value the UI ranks\n * by and the value it prints stay derived from one tier traversal.\n */\nexport function maxRewardPercent(reward: EstimatedReward): number {\n if (reward.payoutType === \"percentage\") return reward.percent;\n if (reward.payoutType === \"tiered\") {\n return reward.tiers.reduce(\n (max, tier) =>\n \"percent\" in tier ? Math.max(max, tier.percent) : max,\n 0\n );\n }\n return 0;\n}\n\n// A reward with no money value (an uncapped percentage, or a percent-only\n// tier set) still renders as \"X %\", so it is worth surfacing. We give it a\n// positive ranking weight derived from its percent but scaled far below any\n// real-money reward, so the two invariants the UI relies on hold: (1) a reward\n// with real money always outranks a percentage-only reward, and (2) a\n// percentage-only reward still outranks a zero-value reward. Together they\n// guarantee the reward the ranking picks is always one we can display.\nconst PERCENT_ONLY_RANK_WEIGHT = 1e-6;\n\n/**\n * Ranking weight used to pick the single most attractive reward to surface.\n *\n * Mirrors {@link getRewardValue} (money value) but lifts a percentage-only\n * reward to a tiny positive weight instead of `0`, so it is never buried\n * behind a zero-value reward when choosing what to display.\n */\nexport function getRewardRank(\n reward: EstimatedReward,\n key: keyof TokenAmountType\n): number {\n const value = getRewardValue(reward, key);\n if (value > 0) return value;\n return maxRewardPercent(reward) * PERCENT_ONLY_RANK_WEIGHT;\n}\n","import type { Currency, EstimatedReward } from \"../types\";\nimport { formatAmount } from \"../utils/format/formatAmount\";\nimport { getCurrencyAmountKey } from \"../utils/format/getCurrencyAmountKey\";\nimport { getSupportedCurrency } from \"../utils/format/getSupportedCurrency\";\nimport { getRewardRank, getRewardValue, maxRewardPercent } from \"./value\";\n\n/**\n * Format an {@link EstimatedReward} into a human-readable string.\n *\n * - `fixed` → e.g. `\"5 €\"`\n * - `percentage` → e.g. `\"10 %\"`\n * - `tiered` → the richest tier: max token amount (e.g. `\"50 €\"`) or, when\n * tiers only carry percentages, the max percent (e.g. `\"10 %\"`)\n *\n * Percentages are always rendered as a `\"X %\"` string: the backend never sends\n * a reference basket, so the reward cannot be resolved to a concrete amount\n * here. Callers that need a worked example use the `example` helpers instead.\n */\nexport function formatEstimatedReward(\n reward: EstimatedReward,\n currency?: Currency\n): string {\n const supportedCurrency = getSupportedCurrency(currency);\n const key = getCurrencyAmountKey(supportedCurrency);\n\n switch (reward.payoutType) {\n case \"fixed\":\n return formatAmount(\n Math.round(reward.amount[key]),\n supportedCurrency\n );\n\n case \"percentage\":\n return `${reward.percent} %`;\n\n case \"tiered\": {\n const maxAmount = getRewardValue(reward, key);\n if (maxAmount > 0) {\n return formatAmount(Math.round(maxAmount), supportedCurrency);\n }\n const maxPercent = maxRewardPercent(reward);\n if (maxPercent > 0) {\n return `${maxPercent} %`;\n }\n return formatAmount(0, supportedCurrency);\n }\n }\n}\n\n/**\n * Format a reward for display, or return `undefined` when it is not worth\n * advertising. Callers rely on `undefined` to hide a badge / fall back to\n * other copy.\n *\n * A reward is hidden only when it carries no displayable value — a `fixed` or\n * `tiered` reward whose money value is `0` (e.g. an unknown token price). An\n * uncapped percentage always renders as `\"X %\"`, since the percent itself is\n * meaningful even without a money value.\n */\nexport function formatRewardOrHide(\n reward: EstimatedReward | undefined,\n currency?: Currency\n): string | undefined {\n if (!reward) return undefined;\n const key = getCurrencyAmountKey(getSupportedCurrency(currency));\n if (getRewardRank(reward, key) <= 0) return undefined;\n return formatEstimatedReward(reward, currency);\n}\n\n/**\n * Replace the `{REWARD}` placeholder in a text string with a reward value.\n * If no reward is provided, returns the text with `{REWARD}` stripped.\n */\nexport function applyRewardPlaceholder(\n text: string,\n reward: string | undefined\n): string {\n return reward\n ? text.replaceAll(\"{REWARD}\", reward)\n : text.replaceAll(\"{REWARD}\", \"\");\n}\n","import type { InteractionTypeKey } from \"../constants/interactionTypes\";\nimport type {\n Currency,\n EstimatedReward,\n MerchantReward,\n TokenAmountType,\n} from \"../types\";\nimport { formatAmount } from \"../utils/format/formatAmount\";\nimport { getCurrencyAmountKey } from \"../utils/format/getCurrencyAmountKey\";\nimport { getSupportedCurrency } from \"../utils/format/getSupportedCurrency\";\nimport { extractMinPurchaseAmount, extractStartDate } from \"./conditions\";\nimport { formatRewardOrHide } from \"./format\";\nimport { getRewardRank } from \"./value\";\n\n/** Reward side a surface cares about: the sharer (`referrer`) or the referred\n * user (`referee`). Drives both campaign ranking and which side is displayed. */\nexport type RewardAudience = \"referrer\" | \"referee\";\n\nexport type DisplayCampaign = {\n campaign: MerchantReward;\n status: \"live\" | \"upcoming\";\n startsAt?: Date;\n};\n\nexport type SelectDisplayCampaignOptions = {\n /** Reference \"now\" for expiry / start gating; defaults to the current time. */\n now?: Date;\n /** Currency the ranking is expressed in; defaults to EUR. */\n currency?: Currency;\n /** When set, only campaigns triggered by this interaction are considered. */\n targetInteraction?: InteractionTypeKey;\n /** Reward side to rank campaigns by; defaults to `\"referrer\"`. */\n audience?: RewardAudience;\n};\n\nfunction isExpired(campaign: MerchantReward, nowMs: number): boolean {\n return (\n campaign.expiresAt != null &&\n new Date(campaign.expiresAt).getTime() <= nowMs\n );\n}\n\nfunction hasStarted(campaign: MerchantReward, nowMs: number): boolean {\n const startsAt = extractStartDate(campaign.conditions);\n return startsAt == null || startsAt.getTime() <= nowMs;\n}\n\nfunction audienceReward(campaign: MerchantReward, audience: RewardAudience) {\n return audience === \"referee\" ? campaign.referee : campaign.referrer;\n}\n\nfunction campaignRank(\n campaign: MerchantReward,\n key: keyof TokenAmountType,\n audience: RewardAudience\n): number {\n const reward = audienceReward(campaign, audience);\n return reward ? getRewardRank(reward, key) : 0;\n}\n\n/**\n * Pick the single campaign a merchant surface should display.\n *\n * Filters out expired (and, when `targetInteraction` is set, non-matching)\n * campaigns, then prefers the highest-ranked *live* campaign — ranked by the\n * `audience` reward side in the requested currency. When none has started yet\n * it falls back to the soonest-starting upcoming campaign (the endpoint does\n * not gate on the start-date condition, so future-start campaigns come\n * through).\n */\nexport function selectDisplayCampaign(\n rewards: readonly MerchantReward[],\n options: SelectDisplayCampaignOptions = {}\n): DisplayCampaign | undefined {\n const nowMs = (options.now ?? new Date()).getTime();\n const audience = options.audience ?? \"referrer\";\n const key = getCurrencyAmountKey(getSupportedCurrency(options.currency));\n\n const matching = options.targetInteraction\n ? rewards.filter(\n (campaign) =>\n campaign.interactionTypeKey === options.targetInteraction\n )\n : rewards;\n const active = matching.filter((campaign) => !isExpired(campaign, nowMs));\n\n const live = active.filter((campaign) => hasStarted(campaign, nowMs));\n if (live.length > 0) {\n const best = live.reduce((a, b) =>\n campaignRank(b, key, audience) > campaignRank(a, key, audience)\n ? b\n : a\n );\n return { campaign: best, status: \"live\" };\n }\n\n const upcoming = active\n .map((campaign) => ({\n campaign,\n startsAt: extractStartDate(campaign.conditions),\n }))\n .filter(\n (entry): entry is { campaign: MerchantReward; startsAt: Date } =>\n entry.startsAt != null\n );\n if (upcoming.length === 0) return undefined;\n\n const soonest = upcoming.reduce((a, b) =>\n b.startsAt.getTime() < a.startsAt.getTime() ? b : a\n );\n return {\n campaign: soonest.campaign,\n status: \"upcoming\",\n startsAt: soonest.startsAt,\n };\n}\n\n/**\n * The single reward a merchant surface should display: its formatted string\n * plus the `payoutType` of the underlying reward, so surfaces can adapt their\n * presentation (e.g. hide percentage rewards, prefix tiered ones with \"Up to\").\n */\nexport type BestReward = {\n /** Display-ready reward string (e.g. `\"5 €\"`, `\"10 %\"`). */\n formatted: string;\n /** Payout type of the selected reward. */\n payoutType: EstimatedReward[\"payoutType\"];\n /**\n * Minimum purchase amount gating the reward, formatted with the requested\n * currency (e.g. `\"10 €\"`), or `undefined` when the campaign sets no\n * minimum.\n */\n minPurchaseAmount?: string;\n /**\n * Whole-day lockup applied before the reward settles, or `undefined` when\n * the campaign has no lockup.\n */\n lockupDurationDays?: number;\n /**\n * Raw referrer/referee rewards of the selected campaign, surfaced so\n * consumers can render the full per-audience breakdown (tier rows,\n * percentage examples) rather than only the headline number.\n */\n referrerReward?: EstimatedReward;\n refereeReward?: EstimatedReward;\n /**\n * Raw minimum purchase value (unformatted), used to build percentage\n * worked-examples consistent with the campaign's gating.\n */\n minPurchaseValue?: number;\n};\n\n/**\n * Pick the best campaign for `options` and resolve its `audience`-side reward\n * to a formatted string plus its `payoutType`, or `undefined` when there is\n * nothing worth showing.\n *\n * Single entry point shared by every \"headline reward\" surface (share button,\n * wallet modal, sharing/install screens) so they all show the same number for\n * a given merchant and can branch on the payout type.\n */\nexport function selectBestReward(\n rewards: readonly MerchantReward[],\n options: SelectDisplayCampaignOptions = {}\n): BestReward | undefined {\n const selected = selectDisplayCampaign(rewards, options);\n if (!selected) return undefined;\n const reward = audienceReward(\n selected.campaign,\n options.audience ?? \"referrer\"\n );\n if (!reward) return undefined;\n const formatted = formatRewardOrHide(reward, options.currency);\n if (!formatted) return undefined;\n\n const minPurchase = extractMinPurchaseAmount(selected.campaign.conditions);\n const minPurchaseAmount =\n minPurchase != null\n ? formatAmount(minPurchase, options.currency)\n : undefined;\n\n const lockupSeconds = selected.campaign.defaultLockupSeconds;\n const lockupDurationDays =\n lockupSeconds && lockupSeconds > 0\n ? Math.round(lockupSeconds / 86_400)\n : undefined;\n\n return {\n formatted,\n payoutType: reward.payoutType,\n minPurchaseAmount,\n lockupDurationDays,\n referrerReward: selected.campaign.referrer,\n refereeReward: selected.campaign.referee,\n minPurchaseValue: minPurchase ?? undefined,\n };\n}\n\n/**\n * Headline reward string for a merchant: picks the best campaign for `options`\n * and formats its `audience`-side reward, or returns `undefined` when there is\n * nothing worth showing.\n *\n * Thin wrapper over {@link selectBestReward} for callers that only need the\n * formatted string.\n */\nexport function formatBestReward(\n rewards: readonly MerchantReward[],\n options: SelectDisplayCampaignOptions = {}\n): string | undefined {\n return selectBestReward(rewards, options)?.formatted;\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{_ as e,g as t,h as n,p as r,y as i}from"./frakContext-Bgidv-Se.js";import{r as a}from"./getCurrencyAmountKey-DQcwjJiB.js";import{Deferred as o,FrakRpcError as s,RpcErrorCodes as c,createRpcClient as l,jsonDecode as u}from"@frak-labs/frame-connector";import{OpenPanel as d}from"@openpanel/web";const f=`nexus-wallet-backup`,p=`frakwallet://`;function m(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 h(){let e=navigator.userAgent;return/Android/i.test(e)&&/Chrome\/\d+/i.test(e)}const g=p.replace(`://`,``);function _(e){return`intent://${e.slice(13)}#Intent;scheme=${g};end`}function v(e,t){let n=t?.timeout??2500,r=!1,i=()=>{document.hidden&&(r=!0)};document.addEventListener(`visibilitychange`,i);let a=h()&&y(e)?_(e):e;window.location.href=a,setTimeout(()=>{document.removeEventListener(`visibilitychange`,i),r||t?.onFallback?.()},n)}function y(e){return e.startsWith(p)}function b(){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 x=b();function S(){return typeof navigator>`u`?!1:x?!0:/Android|webOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)}function C(){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 w=C();function T(e){x&&e.startsWith(`https://`)?window.location.href=`x-safari-https://${e.slice(8)}`:x&&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 E={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 D({walletBaseUrl:e,config:n}){let r=document.querySelector(`#frak-wallet`);r&&r.remove();let a=document.createElement(`iframe`);a.id=E.id,a.name=E.name,a.allow=E.allow,a.style.zIndex=E.style.zIndex.toString(),k({iframe:a,isVisible:!1});let o=n?.walletUrl??e??`https://wallet.frak.id`,s=i();return j(o),j(t(o)),a.src=O({walletUrl:o,clientId:s,preload:n?.preload}),new Promise(e=>{a.addEventListener(`load`,()=>e(a)),document.body.appendChild(a)})}function O({walletUrl:e,clientId:t,preload:n}){let r=`${e}/listener?clientId=${encodeURIComponent(t)}`;return!n||n.length===0?r:`${r}#preload=${n.join(`,`)}`}function k({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 A(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 j(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 M=x&&w;function N(e){e?localStorage.setItem(f,e):localStorage.removeItem(f)}function P(e,t){try{let n=new URL(e);if(!n.searchParams.has(`u`))return e;let r=L(window.location.href,t);return n.searchParams.delete(`u`),n.searchParams.append(`u`,r),n.toString()}catch{return e}}function F(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 I(e){return e.includes(`/common/social`)}function L(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 R(e,t,n,r,i){if(i){let e=P(t,r);window.open(e,`_blank`);return}if(y(t)){let i=P(t,r);v(i,{onFallback:()=>{e.contentWindow?.postMessage({clientLifecycle:`deep-link-failed`,data:{originalUrl:i}},n)}})}else if(M&&I(t))F(r);else{let e=P(t,r);window.location.href=e}}function z({iframe:e,targetOrigin:t}){let n=new o;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`:N(a.backup);break;case`remove-backup`:localStorage.removeItem(f);break;case`show`:case`hide`:k({iframe:e,isVisible:i===`show`});break;case`redirect`:R(e,a.baseRedirectUrl,t,a.mergeToken,a.openInNewTab);break}},isConnected:n.promise}}function B({config:t,iframe:r}){let a=t?.walletUrl??`https://wallet.frak.id`,u=typeof navigator<`u`?navigator.language?.split(`-`)[0]:void 0,f=t.metadata.lang??(u===`en`||u===`fr`?u: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=z({iframe:r,targetOrigin:a}),g=new o,_=Date.now();if(!r.contentWindow)throw new s(c.configError,`The iframe does not have a content window`);let v=l({emittingTransport:r.contentWindow,listeningTransport:window,targetOrigin:a,middleware:[{async onRequest(e,t){if(!await h.isConnected)throw new s(c.clientNotConnected,`The iframe provider isn't connected yet`);return await g.promise,t}}],lifecycleHandlers:{iframeLifecycle:(e,t)=>{h.handleEvent(e)}}}),y=V(v,h),b=async()=>{y(),v.cleanup(),r.remove(),e(),n.clearCache(),n.reset()},x;{console.log(`[Frak SDK] Initializing OpenPanel`),x=new d({apiUrl:`https://op-api.gcp.frak.id`,clientId:`6eacc8d7-49ac-4936-95e9-81ef29449570`,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.2.0`,user_anonymous_client_id:i()}),!0)}),x.setGlobalProperties({sdk_version:`1.2.0`,user_anonymous_client_id:i()}),x.init(),x.track(`sdk_initialized`,{sdk_version:`1.2.0`});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=H({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 V(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 H({config:e,rpcClient:t,lifecycleManager:r,configPromise:a,contextSent:o,openPanel:s}){await r.isConnected,m(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})},d=!1,p=e=>{let n=d?void 0:l;d=!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&&(p(n.getConfig()),o.resolve()),a&&(u(await a),p(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(f);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 U(e){return u(r(e))}async function W({config:e}){let t=G(e),n=await D({config:t});if(!n){console.error(`Failed to create iframe`);return}let r=B({config:t,iframe:n});if(await r.waitForSetup,!await r.waitForConnection){console.error(`Failed to connect to client`);return}return r}function G(e){let t=a(e.metadata?.currency);return{...e,metadata:{...e.metadata,currency:t}}}function K({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{E as a,w as c,v as d,p as f,B as i,S as l,W as n,A as o,U as r,x as s,K as t,T as u};
|
|
2
|
-
//# sourceMappingURL=src-
|
|
1
|
+
import{_ as e,g as t,h as n,p as r,y as i}from"./frakContext-Db64Zo2o.js";import{r as a}from"./getCurrencyAmountKey-C-1W-BRg.js";import{Deferred as o,FrakRpcError as s,RpcErrorCodes as c,createRpcClient as l,jsonDecode as u}from"@frak-labs/frame-connector";import{OpenPanel as d}from"@openpanel/web";const f=`nexus-wallet-backup`,p=`frakwallet://`;function m(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 h(){let e=navigator.userAgent;return/Android/i.test(e)&&/Chrome\/\d+/i.test(e)}const g=p.replace(`://`,``);function _(e){return`intent://${e.slice(13)}#Intent;scheme=${g};end`}function v(e,t){let n=t?.timeout??2500,r=!1,i=()=>{document.hidden&&(r=!0)};document.addEventListener(`visibilitychange`,i);let a=h()&&y(e)?_(e):e;window.location.href=a,setTimeout(()=>{document.removeEventListener(`visibilitychange`,i),r||t?.onFallback?.()},n)}function y(e){return e.startsWith(p)}function b(){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 x=b();function S(){return typeof navigator>`u`?!1:x?!0:/Android|webOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)}function C(){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 w=C();function T(e){x&&e.startsWith(`https://`)?window.location.href=`x-safari-https://${e.slice(8)}`:x&&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 E={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 D({walletBaseUrl:e,config:n}){let r=document.querySelector(`#frak-wallet`);r&&r.remove();let a=document.createElement(`iframe`);a.id=E.id,a.name=E.name,a.allow=E.allow,a.style.zIndex=E.style.zIndex.toString(),k({iframe:a,isVisible:!1});let o=n?.walletUrl??e??`https://wallet.frak.id`,s=i();return j(o),j(t(o)),a.src=O({walletUrl:o,clientId:s,preload:n?.preload}),new Promise(e=>{a.addEventListener(`load`,()=>e(a)),document.body.appendChild(a)})}function O({walletUrl:e,clientId:t,preload:n}){let r=`${e}/listener?clientId=${encodeURIComponent(t)}`;return!n||n.length===0?r:`${r}#preload=${n.join(`,`)}`}function k({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 A(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 j(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 M=x&&w;function N(e){e?localStorage.setItem(f,e):localStorage.removeItem(f)}function P(e,t){try{let n=new URL(e);if(!n.searchParams.has(`u`))return e;let r=L(window.location.href,t);return n.searchParams.delete(`u`),n.searchParams.append(`u`,r),n.toString()}catch{return e}}function F(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 I(e){return e.includes(`/common/social`)}function L(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 R(e,t,n,r,i){if(i){let e=P(t,r);window.open(e,`_blank`);return}if(y(t)){let i=P(t,r);v(i,{onFallback:()=>{e.contentWindow?.postMessage({clientLifecycle:`deep-link-failed`,data:{originalUrl:i}},n)}})}else if(M&&I(t))F(r);else{let e=P(t,r);window.location.href=e}}function z({iframe:e,targetOrigin:t}){let n=new o;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`:N(a.backup);break;case`remove-backup`:localStorage.removeItem(f);break;case`show`:case`hide`:k({iframe:e,isVisible:i===`show`});break;case`redirect`:R(e,a.baseRedirectUrl,t,a.mergeToken,a.openInNewTab);break}},isConnected:n.promise}}function B({config:t,iframe:r}){let a=t?.walletUrl??`https://wallet.frak.id`,u=typeof navigator<`u`?navigator.language?.split(`-`)[0]:void 0,f=t.metadata.lang??(u===`en`||u===`fr`?u: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=z({iframe:r,targetOrigin:a}),g=new o,_=Date.now();if(!r.contentWindow)throw new s(c.configError,`The iframe does not have a content window`);let v=l({emittingTransport:r.contentWindow,listeningTransport:window,targetOrigin:a,middleware:[{async onRequest(e,t){if(!await h.isConnected)throw new s(c.clientNotConnected,`The iframe provider isn't connected yet`);return await g.promise,t}}],lifecycleHandlers:{iframeLifecycle:(e,t)=>{h.handleEvent(e)}}}),y=V(v,h),b=async()=>{y(),v.cleanup(),r.remove(),e(),n.clearCache(),n.reset()},x;{console.log(`[Frak SDK] Initializing OpenPanel`),x=new d({apiUrl:`https://op-api.gcp.frak.id`,clientId:`6eacc8d7-49ac-4936-95e9-81ef29449570`,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.2.0`,user_anonymous_client_id:i()}),!0)}),x.setGlobalProperties({sdk_version:`1.2.0`,user_anonymous_client_id:i()}),x.init(),x.track(`sdk_initialized`,{sdk_version:`1.2.0`});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=H({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 V(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 H({config:e,rpcClient:t,lifecycleManager:r,configPromise:a,contextSent:o,openPanel:s}){await r.isConnected,m(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})},d=!1,p=e=>{let n=d?void 0:l;d=!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&&(p(n.getConfig()),o.resolve()),a&&(u(await a),p(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(f);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 U(e){return u(r(e))}async function W({config:e}){let t=G(e),n=await D({config:t});if(!n){console.error(`Failed to create iframe`);return}let r=B({config:t,iframe:n});if(await r.waitForSetup,!await r.waitForConnection){console.error(`Failed to connect to client`);return}return r}function G(e){let t=a(e.metadata?.currency);return{...e,metadata:{...e.metadata,currency:t}}}function K({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{E as a,w as c,v as d,p as f,B as i,S as l,W as n,A as o,U as r,x as s,K as t,T as u};
|
|
2
|
+
//# sourceMappingURL=src-Bigq2qnT.js.map
|