@frak-labs/core-sdk 1.1.4 → 1.1.5-beta.5b65e64f

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.
Files changed (77) hide show
  1. package/cdn/bundle.js +1 -1
  2. package/dist/actions-Cyn4asew.cjs +2 -0
  3. package/dist/actions-Cyn4asew.cjs.map +1 -0
  4. package/dist/actions-DtPVuk2e.js +2 -0
  5. package/dist/actions-DtPVuk2e.js.map +1 -0
  6. package/dist/actions.cjs +1 -1
  7. package/dist/actions.d.cts +3 -3
  8. package/dist/actions.d.ts +3 -3
  9. package/dist/actions.js +1 -1
  10. package/dist/bundle.cjs +1 -1
  11. package/dist/bundle.d.cts +5 -4
  12. package/dist/bundle.d.ts +5 -4
  13. package/dist/bundle.js +1 -1
  14. package/dist/frakContext-Bgidv-Se.js +2 -0
  15. package/dist/frakContext-Bgidv-Se.js.map +1 -0
  16. package/dist/frakContext-CeVlWFcX.cjs +2 -0
  17. package/dist/frakContext-CeVlWFcX.cjs.map +1 -0
  18. package/dist/getCurrencyAmountKey-DQcwjJiB.js +2 -0
  19. package/dist/getCurrencyAmountKey-DQcwjJiB.js.map +1 -0
  20. package/dist/getCurrencyAmountKey-h2eOiYzv.cjs +2 -0
  21. package/dist/getCurrencyAmountKey-h2eOiYzv.cjs.map +1 -0
  22. package/dist/{index-lcImYQyL.d.ts → index-BCoPwxjq.d.cts} +4 -2
  23. package/dist/{index-Cm1PNC9z.d.ts → index-CiHjUnfc.d.ts} +8 -6
  24. package/dist/{index-Cumn_FNI.d.cts → index-D25z5B9K.d.cts} +8 -6
  25. package/dist/{index-DR4n-QOe.d.cts → index-trhAN7q9.d.ts} +4 -2
  26. package/dist/index.cjs +1 -1
  27. package/dist/index.d.cts +4 -3
  28. package/dist/index.d.ts +4 -3
  29. package/dist/index.js +1 -1
  30. package/dist/merchantInformation--tLnowAN.d.ts +344 -0
  31. package/dist/merchantInformation-BSuahpjl.d.cts +344 -0
  32. package/dist/{openSso-BErs7_Bg.d.cts → openSso-CENLMWJx.d.ts} +4 -279
  33. package/dist/{openSso-DRLH4D9a.d.ts → openSso-CKvkAGts.d.cts} +4 -279
  34. package/dist/rewards.cjs +2 -0
  35. package/dist/rewards.cjs.map +1 -0
  36. package/dist/rewards.d.cts +101 -0
  37. package/dist/rewards.d.ts +101 -0
  38. package/dist/rewards.js +2 -0
  39. package/dist/rewards.js.map +1 -0
  40. package/dist/src-BXtnEE3a.js +2 -0
  41. package/dist/src-BXtnEE3a.js.map +1 -0
  42. package/dist/src-TpX-7MwJ.cjs +2 -0
  43. package/dist/src-TpX-7MwJ.cjs.map +1 -0
  44. package/package.json +16 -3
  45. package/src/actions/getMerchantInformation.test.ts +2 -0
  46. package/src/actions/referral/processReferral.test.ts +4 -4
  47. package/src/actions/referral/processReferral.ts +4 -4
  48. package/src/clients/createIFrameFrakClient.ts +7 -7
  49. package/src/clients/transports/iframeLifecycleManager.ts +5 -21
  50. package/src/config/sdkConfigStore.ts +6 -3
  51. package/src/index.ts +6 -0
  52. package/src/rewards/conditions.test.ts +60 -0
  53. package/src/rewards/conditions.ts +72 -0
  54. package/src/rewards/example.test.ts +92 -0
  55. package/src/rewards/example.ts +57 -0
  56. package/src/rewards/format.test.ts +143 -0
  57. package/src/rewards/format.ts +81 -0
  58. package/src/rewards/index.ts +27 -0
  59. package/src/rewards/select.test.ts +212 -0
  60. package/src/rewards/select.ts +132 -0
  61. package/src/rewards/value.test.ts +89 -0
  62. package/src/rewards/value.ts +73 -0
  63. package/src/types/index.ts +6 -0
  64. package/src/types/rpc/merchantInformation.ts +120 -13
  65. package/src/utils/analytics/events/lifecycle.ts +1 -1
  66. package/src/utils/analytics/events/referral.ts +3 -3
  67. package/src/utils/browser/deepLinkWithFallback.ts +4 -1
  68. package/src/utils/browser/inAppBrowser.ts +2 -2
  69. package/src/utils/cache/withCache.ts +30 -11
  70. package/src/utils/iframe/iframeHelper.test.ts +39 -0
  71. package/src/utils/iframe/iframeHelper.ts +31 -2
  72. package/dist/actions-D5GbNcvt.cjs +0 -1
  73. package/dist/actions-DE2dHqtp.js +0 -1
  74. package/dist/frakContext-Bl17GGa3.cjs +0 -1
  75. package/dist/frakContext-DmI5CaSw.js +0 -1
  76. package/dist/src-5vpUuhfY.js +0 -1
  77. package/dist/src-BQsjMpvA.cjs +0 -1
@@ -1,202 +1,9 @@
1
- import { LifecycleMessage, RpcClient } from "@frak-labs/frame-connector";
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-BSuahpjl.cjs";
2
2
  import { OpenPanel } from "@openpanel/web";
3
3
  import { Address, Hex } from "viem";
4
+ import { LifecycleMessage, RpcClient } from "@frak-labs/frame-connector";
4
5
  import { SiweMessage } from "viem/siwe";
5
6
 
6
- //#region src/types/tracking.d.ts
7
- type UtmParams = {
8
- source?: string;
9
- medium?: string;
10
- campaign?: string;
11
- term?: string;
12
- content?: string;
13
- };
14
- /**
15
- * Attribution parameters appended to outbound sharing URLs.
16
- *
17
- * Defaults are derived from the V2 Frak context when available:
18
- * - `utmSource`: `"frak"`
19
- * - `utmMedium`: `"referral"`
20
- * - `utmCampaign`: merchantId (`context.m`)
21
- * - `via`: `"frak"`
22
- * - `ref`: clientId (`context.c`)
23
- *
24
- * Fields explicitly set here override the defaults. Existing params on the
25
- * base URL are preserved (gap-fill policy) to respect merchant-provided UTMs.
26
- */
27
- type AttributionParams = {
28
- utmSource?: string;
29
- utmMedium?: string;
30
- utmCampaign?: string;
31
- utmContent?: string;
32
- utmTerm?: string;
33
- via?: string;
34
- ref?: string;
35
- };
36
- /**
37
- * Merchant-level attribution defaults.
38
- *
39
- * Same shape as {@link AttributionParams} minus `utmContent`, because
40
- * `utm_content` describes the specific content/creative being shared and is
41
- * inherently per-call or per-product (never a merchant-wide default).
42
- *
43
- * Used as the shape for both:
44
- * - `FrakWalletSdkConfig.attribution` (SDK-side compile-time defaults)
45
- * - Backend merchant-config attribution (dashboard-driven defaults)
46
- */
47
- type AttributionDefaults = Omit<AttributionParams, "utmContent">;
48
- type TrackArrivalParams = {
49
- /** Sharer wallet address. Accepted in both V1 (legacy) and V2 (authenticated sharer) contexts. */referrerWallet?: Address;
50
- referrerClientId?: string;
51
- referrerMerchantId?: string; /** Epoch seconds timestamp from the referral link creation */
52
- referralTimestamp?: number;
53
- };
54
- type TrackArrivalResult = {
55
- success: boolean;
56
- identityGroupId?: string;
57
- referralLinkId?: string;
58
- error?: string;
59
- };
60
- //#endregion
61
- //#region src/types/config.d.ts
62
- /**
63
- * All the currencies available
64
- * @category Config
65
- */
66
- type Currency = "eur" | "usd" | "gbp";
67
- /**
68
- * All the languages available
69
- * @category Config
70
- */
71
- type Language = "fr" | "en";
72
- /**
73
- * Configuration for the Frak Wallet SDK
74
- * @category Config
75
- */
76
- type FrakWalletSdkConfig = {
77
- /**
78
- * The Frak wallet url
79
- * @defaultValue "https://wallet.frak.id"
80
- */
81
- walletUrl?: string;
82
- /**
83
- * Some metadata about your implementation of the Frak SDK
84
- */
85
- metadata: {
86
- /**
87
- * Your application name (will be displayed in a few modals and in SSO)
88
- */
89
- name?: string;
90
- /**
91
- * Your merchant ID from the Frak dashboard (UUID format)
92
- * Used for referral tracking and analytics
93
- * If not provided, will be auto-fetched from the backend using your domain
94
- */
95
- merchantId?: string;
96
- /**
97
- * Language to display in the modal
98
- * If undefined, will default to the browser language
99
- */
100
- lang?: Language;
101
- /**
102
- * The currency to display in the modal
103
- * @defaultValue `"eur"`
104
- */
105
- currency?: Currency;
106
- /**
107
- * The logo URL that will be displayed in a few components
108
- */
109
- logoUrl?: string;
110
- /**
111
- * The homepage link that could be displayed in a few components
112
- */
113
- homepageLink?: string;
114
- };
115
- /**
116
- * Some customization for the modal
117
- */
118
- customizations?: {
119
- /**
120
- * Custom CSS styles to apply to the modals and components
121
- */
122
- css?: `${string}.css`;
123
- /**
124
- * Custom i18n configuration for the modal
125
- */
126
- i18n?: I18nConfig;
127
- };
128
- /**
129
- * The domain name of your application
130
- * @defaultValue window.location.host
131
- */
132
- domain?: string;
133
- /**
134
- * Wait for backend config before rendering components.
135
- * When true (default), components show a spinner until backend config is resolved.
136
- * When false, components render immediately with SDK static config / HTML attributes.
137
- * @defaultValue true
138
- */
139
- waitForBackendConfig?: boolean;
140
- /**
141
- * Default attribution params (UTM / via / ref) appended to outbound
142
- * sharing URLs. Per-call `displaySharingPage` overrides win, then backend
143
- * config, then this SDK-level default. `utm_content` is intentionally
144
- * excluded — it is per-content/per-product, never a merchant-wide default.
145
- */
146
- attribution?: AttributionDefaults;
147
- /**
148
- * Preload specific UI views inside the listener iframe for better UX.
149
- * Default: ["sharing"]
150
- */
151
- preload?: ListenerPreloadOption[];
152
- };
153
- /**
154
- * Custom i18n configuration for the modal
155
- * See [i18next json format](https://www.i18next.com/misc/json-format#i18next-json-v4)
156
- *
157
- * Available variables
158
- * - `{{ productName }}` : The name of your website (`metadata.name`)
159
- * - `{{ productOrigin }}` : The origin url of your website
160
- * - `{{ estimatedReward }}` : The estimated reward for the user (based on the specific `targetInteraction` you can specify, or the max referrer reward if no target interaction is specified)
161
- *
162
- * Context of the translation [see i18n context](https://www.i18next.com/translation-function/context)
163
- * - For modal display, the key of the final action (`sharing`, `reward`, or undefined)
164
- * - For embedded wallet display, the key of the logged in action (`sharing` or undefined)
165
- *
166
- * @example
167
- * ```ts
168
- * // Multi language config
169
- * const multiI18n = {
170
- * fr: {
171
- * "sdk.modal.title": "Titre de modal",
172
- * "sdk.modal.description": "Description de modal, avec {{ estimatedReward }} de gains possible",
173
- * },
174
- * en: "https://example.com/en.json"
175
- * }
176
- *
177
- * // Single language config
178
- * const singleI18n = {
179
- * "sdk.modal.title": "Modal title",
180
- * "sdk.modal.description": "Modal description, with {{ estimatedReward }} of gains possible",
181
- * }
182
- * ```
183
- *
184
- * @category Config
185
- */
186
- type I18nConfig = Record<Language, LocalizedI18nConfig> | LocalizedI18nConfig;
187
- /**
188
- * Options for preloading the listener UI
189
- * @category Config
190
- */
191
- type ListenerPreloadOption = "modal" | "sharing";
192
- /**
193
- * A localized i18n config (inline objects only — URL-based i18n removed)
194
- * @category Config
195
- */
196
- type LocalizedI18nConfig = {
197
- [key: string]: string;
198
- };
199
- //#endregion
200
7
  //#region src/types/resolvedConfig.d.ts
201
8
  /**
202
9
  * Response from the merchant resolve endpoint
@@ -404,19 +211,6 @@ type RedirectRequestEvent = {
404
211
  };
405
212
  };
406
213
  //#endregion
407
- //#region src/constants/interactionTypes.d.ts
408
- /**
409
- * The supported interaction type keys
410
- *
411
- * - `referral` - User arrived via a referral link
412
- * - `create_referral_link` - User created/shared a referral link
413
- * - `purchase` - User completed a purchase
414
- * - `custom.${string}` - Custom interaction type defined per campaign
415
- *
416
- * @inline
417
- */
418
- type InteractionTypeKey = "referral" | "create_referral_link" | "purchase" | `custom.${string}`;
419
- //#endregion
420
214
  //#region src/types/rpc/modal/generic.d.ts
421
215
  /**
422
216
  * Represent a generic modal step type
@@ -978,76 +772,6 @@ type SendInteractionParamsType = {
978
772
  idempotencyKey?: string;
979
773
  };
980
774
  //#endregion
981
- //#region src/types/rpc/merchantInformation.d.ts
982
- /**
983
- * The type for the amount of tokens
984
- */
985
- type TokenAmountType = {
986
- amount: number;
987
- eurAmount: number;
988
- usdAmount: number;
989
- gbpAmount: number;
990
- };
991
- /**
992
- * A tier definition for tiered rewards
993
- */
994
- type RewardTier = {
995
- minValue: number;
996
- maxValue?: number;
997
- amount: TokenAmountType;
998
- };
999
- /**
1000
- * Estimated reward amount — discriminated union by payout type
1001
- *
1002
- * - `fixed`: A known token amount (with fiat equivalents)
1003
- * - `percentage`: A percent of a purchase field (e.g. 5% of purchase_amount), with optional min/max caps
1004
- * - `tiered`: Amount depends on a field value matching tier brackets
1005
- */
1006
- type EstimatedReward = {
1007
- payoutType: "fixed";
1008
- amount: TokenAmountType;
1009
- } | {
1010
- payoutType: "percentage";
1011
- percent: number;
1012
- percentOf: string;
1013
- maxAmount?: TokenAmountType;
1014
- minAmount?: TokenAmountType;
1015
- } | {
1016
- payoutType: "tiered";
1017
- tierField: string;
1018
- tiers: RewardTier[];
1019
- };
1020
- /**
1021
- * Response of the `frak_getMerchantInformation` RPC method
1022
- * @group RPC Schema
1023
- */
1024
- type GetMerchantInformationReturnType = {
1025
- /**
1026
- * Current merchant id
1027
- */
1028
- id: string;
1029
- /**
1030
- * Some metadata
1031
- */
1032
- onChainMetadata: {
1033
- /**
1034
- * Name of the merchant on-chain
1035
- */
1036
- name: string;
1037
- /**
1038
- * Domain of the merchant on-chain
1039
- */
1040
- domain: string;
1041
- };
1042
- rewards: {
1043
- token?: Address;
1044
- campaignId: string;
1045
- interactionTypeKey: InteractionTypeKey;
1046
- referrer?: EstimatedReward;
1047
- referee?: EstimatedReward;
1048
- }[];
1049
- };
1050
- //#endregion
1051
775
  //#region src/types/rpc/userReferralStatus.d.ts
1052
776
  /**
1053
777
  * User referral status returned by `frak_getUserReferralStatus`.
@@ -1387,4 +1111,5 @@ declare const ssoPopupName = "frak-sso";
1387
1111
  */
1388
1112
  declare function openSso(client: FrakClient, inputArgs: OpenSsoParamsType): Promise<OpenSsoReturnType>;
1389
1113
  //#endregion
1390
- export { SdkResolvedConfig as $, ModalRpcStepsResultType as A, OpenSsoReturnType as B, LoggedInEmbeddedView as C, DisplayModalParamsType as D, SharingPageProduct as E, SiweAuthenticateModalStepType as F, FinalModalStepType as G, PrepareSsoReturnType as H, SiweAuthenticateReturnType as I, IFrameLifecycleEvent as J, ModalStepMetadata as K, SiweAuthenticationParams as L, SendTransactionModalStepType as M, SendTransactionReturnType as N, ModalRpcMetadata as O, SendTransactionTxType as P, ResolvedSdkConfig as Q, LoginModalStepType as R, EmbeddedViewActionSharing as S, DisplaySharingPageResultType as T, SsoMetadata as U, PrepareSsoParamsType as V, FinalActionType as W, MerchantConfigResponse as X, ClientLifecycleEvent as Y, ResolvedPlacement as Z, SendInteractionParamsType as _, FrakContextV1 as a, LocalizedI18nConfig as at, LoggedOutEmbeddedView as b, FrakLifecycleEvent as c, TrackArrivalParams as ct, WalletStatusReturnType as d, Currency as et, UserReferralStatusType as f, TokenAmountType as g, RewardTier as h, FrakContext as i, ListenerPreloadOption as it, ModalStepTypes as j, ModalRpcStepsInput as k, IFrameTransport as l, TrackArrivalResult as lt, GetMerchantInformationReturnType as m, ssoPopupFeatures as n, I18nConfig as nt, FrakContextV2 as o, AttributionDefaults as ot, EstimatedReward as p, InteractionTypeKey as q, ssoPopupName as r, Language as rt, FrakClient as s, AttributionParams as st, openSso as t, FrakWalletSdkConfig as tt, IFrameRpcSchema as u, UtmParams as ut, DisplayEmbeddedWalletParamsType as v, DisplaySharingPageParamsType as w, EmbeddedViewActionReferred as x, DisplayEmbeddedWalletResultType as y, OpenSsoParamsType as z };
1114
+ 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 };
1115
+ //# sourceMappingURL=openSso-CKvkAGts.d.cts.map
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./getCurrencyAmountKey-h2eOiYzv.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.replace(`{REWARD}`,t):e.replace(`{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(e,t={}){let n=C(e,t);if(n)return _(x(n.campaign,t.audience??`referrer`),t.currency)}exports.applyRewardPlaceholder=v,exports.buildPercentageExample=d,exports.buildTierExample=f,exports.extractMinPurchaseAmount=o,exports.extractStartDate=s,exports.formatBestReward=w,exports.formatEstimatedReward=g,exports.formatRewardOrHide=_,exports.getRewardValue=p,exports.pickFlatBasket=l,exports.pickTierBasket=u,exports.selectDisplayCampaign=C;
2
+ //# sourceMappingURL=rewards.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rewards.cjs","names":["getSupportedCurrency","getCurrencyAmountKey","formatAmount","getCurrencyAmountKey","getSupportedCurrency"],"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.replace(\"{REWARD}\", reward)\n : text.replace(\"{REWARD}\", \"\");\n}\n","import type { InteractionTypeKey } from \"../constants/interactionTypes\";\nimport type { Currency, MerchantReward, TokenAmountType } from \"../types\";\nimport { getCurrencyAmountKey } from \"../utils/format/getCurrencyAmountKey\";\nimport { getSupportedCurrency } from \"../utils/format/getSupportedCurrency\";\nimport { 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 * 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 * 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.\n */\nexport function formatBestReward(\n rewards: readonly MerchantReward[],\n options: SelectDisplayCampaignOptions = {}\n): string | 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 return formatRewardOrHide(reward, options.currency);\n}\n"],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ import { a as MerchantReward, c as RuleConditions, d as InteractionTypeKey, f as Currency, r as EstimatedReward, u as TokenAmountType } from "./merchantInformation-BSuahpjl.cjs";
2
+
3
+ //#region src/rewards/conditions.d.ts
4
+ declare function extractMinPurchaseAmount(conditions: RuleConditions): number | undefined;
5
+ declare function extractStartDate(conditions: RuleConditions): Date | undefined;
6
+ //#endregion
7
+ //#region src/rewards/example.d.ts
8
+ type PercentageReward = Extract<EstimatedReward, {
9
+ payoutType: "percentage";
10
+ }>;
11
+ type RewardExample = {
12
+ basket: number;
13
+ reward: number;
14
+ };
15
+ declare function pickFlatBasket(minPurchase: number | undefined): number;
16
+ declare function pickTierBasket(minValue: number, maxValue: number | undefined): number;
17
+ declare function buildPercentageExample(reward: PercentageReward, minPurchase: number | undefined): RewardExample | undefined;
18
+ declare function buildTierExample(percent: number, minValue: number, maxValue: number | undefined): RewardExample | undefined;
19
+ //#endregion
20
+ //#region src/rewards/format.d.ts
21
+ /**
22
+ * Format an {@link EstimatedReward} into a human-readable string.
23
+ *
24
+ * - `fixed` → e.g. `"5 €"`
25
+ * - `percentage` → e.g. `"10 %"`
26
+ * - `tiered` → the richest tier: max token amount (e.g. `"50 €"`) or, when
27
+ * tiers only carry percentages, the max percent (e.g. `"10 %"`)
28
+ *
29
+ * Percentages are always rendered as a `"X %"` string: the backend never sends
30
+ * a reference basket, so the reward cannot be resolved to a concrete amount
31
+ * here. Callers that need a worked example use the `example` helpers instead.
32
+ */
33
+ declare function formatEstimatedReward(reward: EstimatedReward, currency?: Currency): string;
34
+ /**
35
+ * Format a reward for display, or return `undefined` when it is not worth
36
+ * advertising. Callers rely on `undefined` to hide a badge / fall back to
37
+ * other copy.
38
+ *
39
+ * A reward is hidden only when it carries no displayable value — a `fixed` or
40
+ * `tiered` reward whose money value is `0` (e.g. an unknown token price). An
41
+ * uncapped percentage always renders as `"X %"`, since the percent itself is
42
+ * meaningful even without a money value.
43
+ */
44
+ declare function formatRewardOrHide(reward: EstimatedReward | undefined, currency?: Currency): string | undefined;
45
+ /**
46
+ * Replace the `{REWARD}` placeholder in a text string with a reward value.
47
+ * If no reward is provided, returns the text with `{REWARD}` stripped.
48
+ */
49
+ declare function applyRewardPlaceholder(text: string, reward: string | undefined): string;
50
+ //#endregion
51
+ //#region src/rewards/select.d.ts
52
+ /** Reward side a surface cares about: the sharer (`referrer`) or the referred
53
+ * user (`referee`). Drives both campaign ranking and which side is displayed. */
54
+ type RewardAudience = "referrer" | "referee";
55
+ type DisplayCampaign = {
56
+ campaign: MerchantReward;
57
+ status: "live" | "upcoming";
58
+ startsAt?: Date;
59
+ };
60
+ type SelectDisplayCampaignOptions = {
61
+ /** Reference "now" for expiry / start gating; defaults to the current time. */now?: Date; /** Currency the ranking is expressed in; defaults to EUR. */
62
+ currency?: Currency; /** When set, only campaigns triggered by this interaction are considered. */
63
+ targetInteraction?: InteractionTypeKey; /** Reward side to rank campaigns by; defaults to `"referrer"`. */
64
+ audience?: RewardAudience;
65
+ };
66
+ /**
67
+ * Pick the single campaign a merchant surface should display.
68
+ *
69
+ * Filters out expired (and, when `targetInteraction` is set, non-matching)
70
+ * campaigns, then prefers the highest-ranked *live* campaign — ranked by the
71
+ * `audience` reward side in the requested currency. When none has started yet
72
+ * it falls back to the soonest-starting upcoming campaign (the endpoint does
73
+ * not gate on the start-date condition, so future-start campaigns come
74
+ * through).
75
+ */
76
+ declare function selectDisplayCampaign(rewards: readonly MerchantReward[], options?: SelectDisplayCampaignOptions): DisplayCampaign | undefined;
77
+ /**
78
+ * Headline reward string for a merchant: picks the best campaign for `options`
79
+ * and formats its `audience`-side reward, or returns `undefined` when there is
80
+ * nothing worth showing.
81
+ *
82
+ * Single entry point shared by every "headline reward" surface (share button,
83
+ * wallet modal, sharing/install screens) so they all show the same number for
84
+ * a given merchant.
85
+ */
86
+ declare function formatBestReward(rewards: readonly MerchantReward[], options?: SelectDisplayCampaignOptions): string | undefined;
87
+ //#endregion
88
+ //#region src/rewards/value.d.ts
89
+ /**
90
+ * Comparable fiat value of a single reward, used to rank rewards against each
91
+ * other.
92
+ *
93
+ * - `fixed` → the token amount in the requested currency
94
+ * - `tiered` → the highest token amount across tiers
95
+ * - `percentage` → the capped (`maxAmount`) value when present, otherwise `0`
96
+ * (an uncapped percentage has no comparable fiat value)
97
+ */
98
+ declare function getRewardValue(reward: EstimatedReward, key: keyof TokenAmountType): number;
99
+ //#endregion
100
+ export { type DisplayCampaign, type RewardAudience, type RewardExample, type SelectDisplayCampaignOptions, applyRewardPlaceholder, buildPercentageExample, buildTierExample, extractMinPurchaseAmount, extractStartDate, formatBestReward, formatEstimatedReward, formatRewardOrHide, getRewardValue, pickFlatBasket, pickTierBasket, selectDisplayCampaign };
101
+ //# sourceMappingURL=rewards.d.cts.map
@@ -0,0 +1,101 @@
1
+ import { a as MerchantReward, c as RuleConditions, d as InteractionTypeKey, f as Currency, r as EstimatedReward, u as TokenAmountType } from "./merchantInformation--tLnowAN.js";
2
+
3
+ //#region src/rewards/conditions.d.ts
4
+ declare function extractMinPurchaseAmount(conditions: RuleConditions): number | undefined;
5
+ declare function extractStartDate(conditions: RuleConditions): Date | undefined;
6
+ //#endregion
7
+ //#region src/rewards/example.d.ts
8
+ type PercentageReward = Extract<EstimatedReward, {
9
+ payoutType: "percentage";
10
+ }>;
11
+ type RewardExample = {
12
+ basket: number;
13
+ reward: number;
14
+ };
15
+ declare function pickFlatBasket(minPurchase: number | undefined): number;
16
+ declare function pickTierBasket(minValue: number, maxValue: number | undefined): number;
17
+ declare function buildPercentageExample(reward: PercentageReward, minPurchase: number | undefined): RewardExample | undefined;
18
+ declare function buildTierExample(percent: number, minValue: number, maxValue: number | undefined): RewardExample | undefined;
19
+ //#endregion
20
+ //#region src/rewards/format.d.ts
21
+ /**
22
+ * Format an {@link EstimatedReward} into a human-readable string.
23
+ *
24
+ * - `fixed` → e.g. `"5 €"`
25
+ * - `percentage` → e.g. `"10 %"`
26
+ * - `tiered` → the richest tier: max token amount (e.g. `"50 €"`) or, when
27
+ * tiers only carry percentages, the max percent (e.g. `"10 %"`)
28
+ *
29
+ * Percentages are always rendered as a `"X %"` string: the backend never sends
30
+ * a reference basket, so the reward cannot be resolved to a concrete amount
31
+ * here. Callers that need a worked example use the `example` helpers instead.
32
+ */
33
+ declare function formatEstimatedReward(reward: EstimatedReward, currency?: Currency): string;
34
+ /**
35
+ * Format a reward for display, or return `undefined` when it is not worth
36
+ * advertising. Callers rely on `undefined` to hide a badge / fall back to
37
+ * other copy.
38
+ *
39
+ * A reward is hidden only when it carries no displayable value — a `fixed` or
40
+ * `tiered` reward whose money value is `0` (e.g. an unknown token price). An
41
+ * uncapped percentage always renders as `"X %"`, since the percent itself is
42
+ * meaningful even without a money value.
43
+ */
44
+ declare function formatRewardOrHide(reward: EstimatedReward | undefined, currency?: Currency): string | undefined;
45
+ /**
46
+ * Replace the `{REWARD}` placeholder in a text string with a reward value.
47
+ * If no reward is provided, returns the text with `{REWARD}` stripped.
48
+ */
49
+ declare function applyRewardPlaceholder(text: string, reward: string | undefined): string;
50
+ //#endregion
51
+ //#region src/rewards/select.d.ts
52
+ /** Reward side a surface cares about: the sharer (`referrer`) or the referred
53
+ * user (`referee`). Drives both campaign ranking and which side is displayed. */
54
+ type RewardAudience = "referrer" | "referee";
55
+ type DisplayCampaign = {
56
+ campaign: MerchantReward;
57
+ status: "live" | "upcoming";
58
+ startsAt?: Date;
59
+ };
60
+ type SelectDisplayCampaignOptions = {
61
+ /** Reference "now" for expiry / start gating; defaults to the current time. */now?: Date; /** Currency the ranking is expressed in; defaults to EUR. */
62
+ currency?: Currency; /** When set, only campaigns triggered by this interaction are considered. */
63
+ targetInteraction?: InteractionTypeKey; /** Reward side to rank campaigns by; defaults to `"referrer"`. */
64
+ audience?: RewardAudience;
65
+ };
66
+ /**
67
+ * Pick the single campaign a merchant surface should display.
68
+ *
69
+ * Filters out expired (and, when `targetInteraction` is set, non-matching)
70
+ * campaigns, then prefers the highest-ranked *live* campaign — ranked by the
71
+ * `audience` reward side in the requested currency. When none has started yet
72
+ * it falls back to the soonest-starting upcoming campaign (the endpoint does
73
+ * not gate on the start-date condition, so future-start campaigns come
74
+ * through).
75
+ */
76
+ declare function selectDisplayCampaign(rewards: readonly MerchantReward[], options?: SelectDisplayCampaignOptions): DisplayCampaign | undefined;
77
+ /**
78
+ * Headline reward string for a merchant: picks the best campaign for `options`
79
+ * and formats its `audience`-side reward, or returns `undefined` when there is
80
+ * nothing worth showing.
81
+ *
82
+ * Single entry point shared by every "headline reward" surface (share button,
83
+ * wallet modal, sharing/install screens) so they all show the same number for
84
+ * a given merchant.
85
+ */
86
+ declare function formatBestReward(rewards: readonly MerchantReward[], options?: SelectDisplayCampaignOptions): string | undefined;
87
+ //#endregion
88
+ //#region src/rewards/value.d.ts
89
+ /**
90
+ * Comparable fiat value of a single reward, used to rank rewards against each
91
+ * other.
92
+ *
93
+ * - `fixed` → the token amount in the requested currency
94
+ * - `tiered` → the highest token amount across tiers
95
+ * - `percentage` → the capped (`maxAmount`) value when present, otherwise `0`
96
+ * (an uncapped percentage has no comparable fiat value)
97
+ */
98
+ declare function getRewardValue(reward: EstimatedReward, key: keyof TokenAmountType): number;
99
+ //#endregion
100
+ export { type DisplayCampaign, type RewardAudience, type RewardExample, type SelectDisplayCampaignOptions, applyRewardPlaceholder, buildPercentageExample, buildTierExample, extractMinPurchaseAmount, extractStartDate, formatBestReward, formatEstimatedReward, formatRewardOrHide, getRewardValue, pickFlatBasket, pickTierBasket, selectDisplayCampaign };
101
+ //# sourceMappingURL=rewards.d.ts.map
@@ -0,0 +1,2 @@
1
+ import{n as e,r as t,t as n}from"./getCurrencyAmountKey-DQcwjJiB.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.replace(`{REWARD}`,t):e.replace(`{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(e,t={}){let n=T(e,t);if(n)return y(C(n.campaign,t.audience??`referrer`),t.currency)}export{b as applyRewardPlaceholder,p as buildPercentageExample,m as buildTierExample,c as extractMinPurchaseAmount,l as extractStartDate,E as formatBestReward,v as formatEstimatedReward,y as formatRewardOrHide,h as getRewardValue,d as pickFlatBasket,f as pickTierBasket,T as selectDisplayCampaign};
2
+ //# sourceMappingURL=rewards.js.map
@@ -0,0 +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.replace(\"{REWARD}\", reward)\n : text.replace(\"{REWARD}\", \"\");\n}\n","import type { InteractionTypeKey } from \"../constants/interactionTypes\";\nimport type { Currency, MerchantReward, TokenAmountType } from \"../types\";\nimport { getCurrencyAmountKey } from \"../utils/format/getCurrencyAmountKey\";\nimport { getSupportedCurrency } from \"../utils/format/getSupportedCurrency\";\nimport { 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 * 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 * 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.\n */\nexport function formatBestReward(\n rewards: readonly MerchantReward[],\n options: SelectDisplayCampaignOptions = {}\n): string | 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 return formatRewardOrHide(reward, options.currency);\n}\n"],"mappings":"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"}