@frak-labs/core-sdk 0.2.0 → 0.2.1-beta.06c52c98

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 (67) hide show
  1. package/README.md +1 -2
  2. package/cdn/bundle.js +55 -3
  3. package/dist/actions.cjs +1 -1
  4. package/dist/actions.d.cts +2 -2
  5. package/dist/actions.d.ts +2 -2
  6. package/dist/actions.js +1 -1
  7. package/dist/bundle.cjs +1 -1
  8. package/dist/bundle.d.cts +4 -4
  9. package/dist/bundle.d.ts +4 -4
  10. package/dist/bundle.js +1 -1
  11. package/dist/{computeLegacyProductId-Raks6FXg.d.cts → computeLegacyProductId-BP-ciVsp.d.cts} +73 -88
  12. package/dist/{computeLegacyProductId-BkyJ4rEY.d.ts → computeLegacyProductId-DiJd7RNo.d.ts} +73 -88
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.cts +3 -3
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +1 -1
  17. package/dist/{openSso-BCJGchIb.d.cts → openSso-B8v3Vtnh.d.ts} +157 -52
  18. package/dist/{openSso-DG-_9CED.d.ts → openSso-n_B4LSuW.d.cts} +157 -52
  19. package/dist/setupClient-Dr_UYfTD.cjs +13 -0
  20. package/dist/setupClient-TuhDjVJx.js +13 -0
  21. package/dist/siweAuthenticate-0UPcUqI1.js +1 -0
  22. package/dist/{siweAuthenticate-Btem4QHs.d.ts → siweAuthenticate-CDCsp8EJ.d.ts} +35 -36
  23. package/dist/siweAuthenticate-CfQibjZR.cjs +1 -0
  24. package/dist/{siweAuthenticate-BH7Dn7nZ.d.cts → siweAuthenticate-yITE-iKh.d.cts} +35 -36
  25. package/dist/trackEvent-5j5kkOCj.js +1 -0
  26. package/dist/trackEvent-B2uom25e.cjs +1 -0
  27. package/package.json +8 -8
  28. package/src/actions/displayEmbeddedWallet.ts +6 -2
  29. package/src/actions/displayModal.ts +6 -2
  30. package/src/actions/ensureIdentity.ts +2 -2
  31. package/src/actions/referral/processReferral.test.ts +109 -125
  32. package/src/actions/referral/processReferral.ts +134 -180
  33. package/src/actions/referral/referralInteraction.test.ts +3 -5
  34. package/src/actions/referral/referralInteraction.ts +2 -7
  35. package/src/actions/trackPurchaseStatus.test.ts +32 -20
  36. package/src/actions/trackPurchaseStatus.ts +3 -5
  37. package/src/actions/wrapper/modalBuilder.test.ts +4 -2
  38. package/src/actions/wrapper/modalBuilder.ts +6 -8
  39. package/src/clients/createIFrameFrakClient.ts +146 -25
  40. package/src/clients/transports/iframeLifecycleManager.test.ts +0 -80
  41. package/src/clients/transports/iframeLifecycleManager.ts +0 -44
  42. package/src/index.ts +8 -3
  43. package/src/types/config.ts +10 -3
  44. package/src/types/context.ts +48 -6
  45. package/src/types/index.ts +8 -2
  46. package/src/types/lifecycle/client.ts +22 -27
  47. package/src/types/lifecycle/iframe.ts +0 -8
  48. package/src/types/resolvedConfig.ts +104 -0
  49. package/src/types/rpc/interaction.ts +9 -0
  50. package/src/types/rpc.ts +7 -5
  51. package/src/types/tracking.ts +5 -34
  52. package/src/utils/FrakContext.test.ts +270 -186
  53. package/src/utils/FrakContext.ts +78 -56
  54. package/src/utils/backendUrl.test.ts +2 -2
  55. package/src/utils/backendUrl.ts +1 -1
  56. package/src/utils/index.ts +1 -5
  57. package/src/utils/sdkConfigStore.test.ts +405 -0
  58. package/src/utils/sdkConfigStore.ts +277 -0
  59. package/src/utils/sso.ts +3 -7
  60. package/dist/setupClient-CQrMDGyZ.js +0 -13
  61. package/dist/setupClient-Ccv3XxwL.cjs +0 -13
  62. package/dist/siweAuthenticate-BJHbtty4.js +0 -1
  63. package/dist/siweAuthenticate-Cwj3HP0m.cjs +0 -1
  64. package/dist/trackEvent-M2RLTQ2p.js +0 -1
  65. package/dist/trackEvent-T_R9ER2S.cjs +0 -1
  66. package/src/utils/merchantId.test.ts +0 -653
  67. package/src/utils/merchantId.ts +0 -143
@@ -1,13 +1,14 @@
1
- import { A as SiweAuthenticateReturnType, D as SendTransactionReturnType, E as SendTransactionModalStepType, F as PrepareSsoParamsType, I as PrepareSsoReturnType, M as LoginModalStepType, R as FinalActionType, S as ModalRpcMetadata, T as ModalStepTypes, a as FrakClient, d as GetMerchantInformationReturnType, g as DisplayEmbeddedWalletResultType, h as DisplayEmbeddedWalletParamsType, i as FrakContext, j as SiweAuthenticationParams, l as WalletStatusReturnType, m as SendInteractionParamsType, w as ModalRpcStepsResultType, x as DisplayModalParamsType, z as FinalModalStepType } from "./openSso-BCJGchIb.cjs";
1
+ import { A as SendTransactionModalStepType, B as PrepareSsoReturnType, E as ModalRpcMetadata, F as SiweAuthenticationParams, H as FinalActionType, I as LoginModalStepType, O as ModalRpcStepsResultType, P as SiweAuthenticateReturnType, T as DisplayModalParamsType, U as FinalModalStepType, b as DisplayEmbeddedWalletResultType, h as GetMerchantInformationReturnType, i as FrakContext, j as SendTransactionReturnType, k as ModalStepTypes, l as FrakClient, p as WalletStatusReturnType, v as SendInteractionParamsType, y as DisplayEmbeddedWalletParamsType, z as PrepareSsoParamsType } from "./openSso-n_B4LSuW.cjs";
2
2
 
3
3
  //#region src/actions/displayEmbeddedWallet.d.ts
4
4
  /**
5
5
  * Function used to display the Frak embedded wallet popup
6
6
  * @param client - The current Frak Client
7
7
  * @param params - The parameter used to customise the embedded wallet
8
+ * @param placement - Optional placement ID to associate with this display request
8
9
  * @returns The embedded wallet display result
9
10
  */
10
- declare function displayEmbeddedWallet(client: FrakClient, params: DisplayEmbeddedWalletParamsType): Promise<DisplayEmbeddedWalletResultType>;
11
+ declare function displayEmbeddedWallet(client: FrakClient, params: DisplayEmbeddedWalletParamsType, placement?: string): Promise<DisplayEmbeddedWalletResultType>;
11
12
  //#endregion
12
13
  //#region src/actions/displayModal.d.ts
13
14
  /**
@@ -16,6 +17,7 @@ declare function displayEmbeddedWallet(client: FrakClient, params: DisplayEmbedd
16
17
  * @param args
17
18
  * @param args.steps - The different steps of the modal
18
19
  * @param args.metadata - The metadata for the modal (customization, etc)
20
+ * @param placement - Optional placement ID to associate with this modal display
19
21
  * @returns The result of each modal steps
20
22
  *
21
23
  * @description This function will display a modal to the user with the provided steps and metadata.
@@ -115,7 +117,7 @@ declare function displayEmbeddedWallet(client: FrakClient, params: DisplayEmbedd
115
117
  declare function displayModal<T extends ModalStepTypes[] = ModalStepTypes[]>(client: FrakClient, {
116
118
  steps,
117
119
  metadata
118
- }: DisplayModalParamsType<T>): Promise<ModalRpcStepsResultType<T>>;
120
+ }: DisplayModalParamsType<T>, placement?: string): Promise<ModalRpcStepsResultType<T>>;
119
121
  //#endregion
120
122
  //#region src/actions/ensureIdentity.d.ts
121
123
  /**
@@ -188,61 +190,60 @@ declare function prepareSso(client: FrakClient, args: PrepareSsoParamsType): Pro
188
190
  //#endregion
189
191
  //#region src/actions/referral/processReferral.d.ts
190
192
  /**
191
- * The different states of the referral process
192
- * @inline
193
- */
194
- type ReferralState = "idle" | "processing" | "success" | "no-wallet" | "error" | "no-referrer" | "self-referral";
195
- /**
196
- * Options for the referral auto-interaction process
193
+ * Options for the referral auto-interaction process.
197
194
  */
198
195
  type ProcessReferralOptions = {
199
196
  /**
200
- * If we want to always append the url with the frak context or not
197
+ * If true, always replace the URL with the current user's referral context
198
+ * so the next visitor gets referred by this user.
201
199
  * @defaultValue false
202
200
  */
203
201
  alwaysAppendUrl?: boolean;
202
+ /**
203
+ * Merchant ID for building the current user's referral context.
204
+ * Required when `alwaysAppendUrl` is true and the incoming context is V1.
205
+ * For V2 contexts, the merchantId is already embedded in the context.
206
+ */
207
+ merchantId?: string;
204
208
  };
205
209
  /**
206
- * This function handle all the heavy lifting of the referral interaction process
207
- * 1. Check if the user has been referred or not (if not, early exit)
208
- * 2. Then check if the user is logged in or not
209
- * 2.1 If not logged in, try a soft login, if it fail, display a modal for the user to login
210
- * 3. Check if that's not a self-referral (if yes, early exit)
211
- * 4. Track the referral event
212
- * 5. Update the current url with the right data
213
- * 6. Return the resulting referral state
210
+ * The different states of the referral process.
211
+ * @inline
212
+ */
213
+ type ReferralState = "idle" | "processing" | "success" | "no-referrer" | "self-referral";
214
+ /**
215
+ * Handle the full referral interaction flow:
214
216
  *
215
- * If any error occurs during the process, the function will catch it and return an error state
217
+ * 1. Check if the user has been referred (if not, early exit)
218
+ * 2. Preflight self-referral check (if yes, early exit)
219
+ * 3. Track the arrival event
220
+ * 4. Replace the current URL with the user's own referral context
221
+ * 5. Return the resulting referral state
216
222
  *
217
223
  * @param client - The current Frak Client
218
224
  * @param args
219
225
  * @param args.walletStatus - The current user wallet status
220
- * @param args.frakContext - The current frak context
221
- * @param args.modalConfig - The modal configuration to display if the user is not logged in
222
- * @param args.options - Some options for the referral interaction
223
- * @returns A promise with the resulting referral state
226
+ * @param args.frakContext - The referral context parsed from the URL
227
+ * @param args.options - Options for URL replacement and merchant context
228
+ * @returns The referral state
224
229
  *
225
- * @see {@link displayModal} for more details about the displayed modal
226
- * @see {@link @frak-labs/core-sdk!ModalStepTypes} for more details on each modal steps types
230
+ * @see {@link @frak-labs/core-sdk!ModalStepTypes} for modal step types
227
231
  */
228
232
  declare function processReferral(client: FrakClient, {
229
233
  walletStatus,
230
234
  frakContext,
231
- modalConfig,
232
235
  options
233
236
  }: {
234
237
  walletStatus?: WalletStatusReturnType;
235
- frakContext?: Partial<FrakContext> | null;
236
- modalConfig?: DisplayEmbeddedWalletParamsType;
238
+ frakContext?: FrakContext | null;
237
239
  options?: ProcessReferralOptions;
238
- }): Promise<ReferralState>;
240
+ }): ReferralState;
239
241
  //#endregion
240
242
  //#region src/actions/referral/referralInteraction.d.ts
241
243
  /**
242
244
  * Function used to handle referral interactions
243
245
  * @param client - The current Frak Client
244
246
  * @param args
245
- * @param args.modalConfig - The modal configuration to display if the user is not logged in
246
247
  * @param args.options - Some options for the referral interaction
247
248
  *
248
249
  * @returns A promise with the resulting referral state, or undefined in case of an error
@@ -250,15 +251,12 @@ declare function processReferral(client: FrakClient, {
250
251
  * @description This function will automatically handle the referral interaction process
251
252
  *
252
253
  * @see {@link processReferral} for more details on the automatic referral handling process
253
- * @see {@link @frak-labs/core-sdk!ModalStepTypes} for more details on each modal steps types
254
254
  */
255
255
  declare function referralInteraction(client: FrakClient, {
256
- modalConfig,
257
256
  options
258
257
  }?: {
259
- modalConfig?: DisplayEmbeddedWalletParamsType;
260
258
  options?: ProcessReferralOptions;
261
- }): Promise<("no-referrer" | "self-referral" | "success" | "idle" | "processing" | "no-wallet" | "error") | undefined>;
259
+ }): Promise<("idle" | "processing" | "success" | "no-referrer" | "self-referral") | undefined>;
262
260
  //#endregion
263
261
  //#region src/actions/sendInteraction.d.ts
264
262
  /**
@@ -326,7 +324,7 @@ declare function sendInteraction(client: FrakClient, params: SendInteractionPara
326
324
  * }
327
325
  *
328
326
  * @remarks
329
- * - Merchant id is resolved in this order: explicit `args.merchantId`, `frak-merchant-id` from session storage, then `fetchMerchantId()`.
327
+ * - Merchant id is resolved in this order: explicit `args.merchantId`, then `sdkConfigStore.resolveMerchantId()` (config store sessionStorage → backend fetch).
330
328
  * - This function supports anonymous users and will use the `x-frak-client-id` header when available.
331
329
  * - At least one identity source must exist (`frak-wallet-interaction-token` or `x-frak-client-id`), otherwise the tracking request is skipped.
332
330
  * - This function will print a warning if used in a non-browser environment or if no identity / merchant id can be resolved.
@@ -384,8 +382,9 @@ type ModalStepBuilder<Steps extends ModalStepTypes[] = ModalStepTypes[]> = {
384
382
  /**
385
383
  * Display the modal
386
384
  * @param metadataOverride - Function returning optional metadata to override the current modal metadata
385
+ * @param placement - Optional placement ID to associate with this modal display
387
386
  */
388
- display: (metadataOverride?: (current?: ModalRpcMetadata) => ModalRpcMetadata | undefined) => Promise<ModalRpcStepsResultType<Steps>>;
387
+ display: (metadataOverride?: (current?: ModalRpcMetadata) => ModalRpcMetadata | undefined, placement?: string) => Promise<ModalRpcStepsResultType<Steps>>;
389
388
  };
390
389
  /**
391
390
  * Represent the output type of the modal builder
@@ -0,0 +1 @@
1
+ import{bytesToHex as e,hexToBytes as t,isAddress as n,keccak256 as r,toHex as i}from"viem";import{jsonDecode as a,jsonEncode as o}from"@frak-labs/frame-connector";const s=`frak-client-id`;function c(){return typeof crypto<`u`&&crypto.randomUUID?crypto.randomUUID():`xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e===`x`?t:t&3|8).toString(16)})}function l(){if(typeof window>`u`||!window.localStorage)return console.warn(`[Frak SDK] No Window / localStorage available to save the clientId`),c();let e=localStorage.getItem(s);return e||(e=c(),localStorage.setItem(s,e)),e}function u({domain:e}={}){return r(i((e??window.location.host).replace(`www.`,``)))}function d(e){return btoa(Array.from(e,e=>String.fromCharCode(e)).join(``)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``)}function f(e){let t=e.length%4;return Uint8Array.from(atob(e.replace(/-/g,`+`).replace(/_/g,`/`).padEnd(e.length+(t===0?0:4-t),`=`)),e=>e.charCodeAt(0))}function p(e){return d(o(e))}function m(e,t,n,r,i,a){let o=p(ee({redirectUrl:t.redirectUrl,directExit:t.directExit,lang:t.lang,merchantId:n,metadata:{name:r,css:a,logoUrl:t.metadata?.logoUrl,homepageLink:t.metadata?.homepageLink},clientId:i})),s=new URL(e);return s.pathname=`/sso`,s.searchParams.set(`p`,o),s.toString()}function ee(e){return{r:e.redirectUrl,cId:e.clientId,d:e.directExit,l:e.lang,m:e.merchantId,md:{n:e.metadata?.name,css:e.metadata?.css,l:e.metadata?.logoUrl,h:e.metadata?.homepageLink}}}const h=`menubar=no,status=no,scrollbars=no,fullscreen=no,width=500, height=800`,g=`frak-sso`;async function _(e,t){let{metadata:n,customizations:r,walletUrl:i}=e.config;if(t.openInSameWindow??!!t.redirectUrl)return await e.request({method:`frak_openSso`,params:[t,n.name,r?.css]});let a=t.ssoPopupUrl??m(i??`https://wallet.frak.id`,t,u(),n.name,l(),r?.css),o=window.open(a,g,h);if(!o)throw Error(`Popup was blocked. Please allow popups for this site.`);return o.focus(),await e.request({method:`frak_openSso`,params:[t,n.name,r?.css]})??{}}const v=`https://backend.frak.id`;function y(e){return e.includes(`localhost:3000`)||e.includes(`localhost:3010`)}function b(e){return y(e)?`https://localhost:3030`:e.includes(`wallet-dev.frak.id`)||e.includes(`wallet.gcp-dev.frak.id`)?`https://backend.gcp-dev.frak.id`:v}function x(e){if(e)return b(e);if(typeof window<`u`){let e=window.FrakSetup?.client?.config?.walletUrl;if(e)return b(e)}return v}function S(e){return a(f(e))}function C(e){return`r`in e&&!(`v`in e)}function w(e){return`v`in e&&e.v===2}const T=`fCtx`;function E(e){if(e)try{return w(e)?!e.c||!e.m||!e.t?void 0:p({v:2,c:e.c,m:e.m,t:e.t}):d(t(e.r))}catch(t){console.error(`Error compressing Frak context`,{e:t,context:e})}}function D(t){if(!(!t||t.length===0))try{let r=S(t);if(r&&typeof r==`object`&&r.v===2)return r.c&&r.m&&r.t?{v:2,c:r.c,m:r.m,t:r.t}:void 0;let i=e(f(t),{size:20});if(n(i))return{r:i}}catch(e){console.error(`Error decompressing Frak context`,{e,context:t})}}function O({url:e}){if(!e)return null;let t=new URL(e).searchParams.get(T);return t?D(t):null}function k({url:e,context:t}){if(!e)return null;let n=E(t);if(!n)return null;let r=new URL(e);return r.searchParams.set(T,n),r.toString()}function A(e){let t=new URL(e);return t.searchParams.delete(T),t.toString()}function j({url:e,context:t}){if(!window.location?.href||typeof window>`u`){console.error(`No window found, can't update context`);return}let n=e??window.location.href,r;r=t===null?A(n):k({url:n,context:t}),r&&window.history.replaceState(null,``,r.toString())}const M={compress:E,decompress:D,parse:O,update:k,remove:A,replaceUrl:j},N=`__frakSdkConfig`,P=`frak-config-cache`,F=`frak-merchant-id`,I={key:P},L=typeof window<`u`;function R(){return{isResolved:!1,merchantId:``}}let z=null;function B(){if(!L)return null;try{let e=localStorage.getItem(I.key);if(!e)return null;let t=JSON.parse(e);return t.config?.isResolved?(z=t,t):null}catch{return null}}function V(){return(z??B())?.config}function H(){let e=z??B();return e?Date.now()-e.timestamp<3e4:!1}function U(e){if(!(!L||!e.isResolved))try{let t={config:e,timestamp:Date.now()};localStorage.setItem(I.key,JSON.stringify(t)),z=t}catch{}}function W(){if(L){z=null;try{localStorage.removeItem(I.key)}catch{}}}function G(){L&&(window[N]||(window[N]=V()??R()))}G();function K(){return L?window[N]??R():R()}function q(e){L&&window.dispatchEvent(new CustomEvent(`frak:config`,{detail:e}))}function J(e){return e??(L?window.location.hostname:``)}const Y=new Map,X=new Map;function Z(e,t){return`${e}:${t??``}`}async function Q(e,t,n){try{let r=x(t),i=n?`&lang=${encodeURIComponent(n)}`:``,a=await fetch(`${r}/user/merchant/resolve?domain=${encodeURIComponent(e)}${i}`);if(!a.ok){console.warn(`[Frak SDK] Merchant lookup failed for domain ${e}: ${a.status}`);return}let o=await a.json(),s=Z(e,n);if(Y.set(s,o),L)try{sessionStorage.setItem(F,o.merchantId)}catch{}return o}catch(e){console.warn(`[Frak SDK] Failed to fetch merchant config:`,e);return}}const $={getConfig:K,get isResolved(){return K().isResolved},get isCacheFresh(){return H()},setCacheScope(e,t){I.key=`${P}:${`${e}:${t??``}`}`,z=null},setConfig(e){if(L&&(window[N]=e),U(e),q(e),L&&e.merchantId)try{sessionStorage.setItem(F,e.merchantId)}catch{}},reset(){let e=V()??R();L&&(window[N]=e),q(e)},clearCache(){if(W(),Y.clear(),X.clear(),L)try{sessionStorage.removeItem(F)}catch{}},resolve(e,t,n){let r=J(e);if(!r)return Promise.resolve(void 0);let i=Z(r,n);if(Y.has(i))return Promise.resolve(Y.get(i));let a=X.get(i);if(a)return a;let o=Q(r,t,n).then(e=>(X.delete(i),e));return X.set(i,o),o},getMerchantId(){let e=K();if(e.isResolved&&e.merchantId)return e.merchantId;if(L)try{return sessionStorage.getItem(F)??void 0}catch{}},async resolveMerchantId(e,t){return $.getMerchantId()||(await $.resolve(e,t))?.merchantId}};function te(e,t,n={}){if(!e){console.debug(`[Frak] No client provided, skipping event tracking`);return}try{e.openPanel?.track(t,n)}catch(e){console.debug(`[Frak] Failed to track event:`,t,e)}}export{w as a,_ as c,m as d,p as f,l as g,u as h,C as i,h as l,d as m,$ as n,S as o,f as p,M as r,x as s,te as t,g as u};
@@ -0,0 +1 @@
1
+ let e=require(`viem`),t=require(`@frak-labs/frame-connector`);const n=`frak-client-id`;function r(){return typeof crypto<`u`&&crypto.randomUUID?crypto.randomUUID():`xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e===`x`?t:t&3|8).toString(16)})}function i(){if(typeof window>`u`||!window.localStorage)return console.warn(`[Frak SDK] No Window / localStorage available to save the clientId`),r();let e=localStorage.getItem(n);return e||(e=r(),localStorage.setItem(n,e)),e}function a({domain:t}={}){return(0,e.keccak256)((0,e.toHex)((t??window.location.host).replace(`www.`,``)))}function o(e){return btoa(Array.from(e,e=>String.fromCharCode(e)).join(``)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``)}function s(e){let t=e.length%4;return Uint8Array.from(atob(e.replace(/-/g,`+`).replace(/_/g,`/`).padEnd(e.length+(t===0?0:4-t),`=`)),e=>e.charCodeAt(0))}function c(e){return o((0,t.jsonEncode)(e))}function l(e,t,n,r,i,a){let o=c(u({redirectUrl:t.redirectUrl,directExit:t.directExit,lang:t.lang,merchantId:n,metadata:{name:r,css:a,logoUrl:t.metadata?.logoUrl,homepageLink:t.metadata?.homepageLink},clientId:i})),s=new URL(e);return s.pathname=`/sso`,s.searchParams.set(`p`,o),s.toString()}function u(e){return{r:e.redirectUrl,cId:e.clientId,d:e.directExit,l:e.lang,m:e.merchantId,md:{n:e.metadata?.name,css:e.metadata?.css,l:e.metadata?.logoUrl,h:e.metadata?.homepageLink}}}const d=`menubar=no,status=no,scrollbars=no,fullscreen=no,width=500, height=800`,f=`frak-sso`;async function p(e,t){let{metadata:n,customizations:r,walletUrl:o}=e.config;if(t.openInSameWindow??!!t.redirectUrl)return await e.request({method:`frak_openSso`,params:[t,n.name,r?.css]});let s=t.ssoPopupUrl??l(o??`https://wallet.frak.id`,t,a(),n.name,i(),r?.css),c=window.open(s,f,d);if(!c)throw Error(`Popup was blocked. Please allow popups for this site.`);return c.focus(),await e.request({method:`frak_openSso`,params:[t,n.name,r?.css]})??{}}const m=`https://backend.frak.id`;function h(e){return e.includes(`localhost:3000`)||e.includes(`localhost:3010`)}function g(e){return h(e)?`https://localhost:3030`:e.includes(`wallet-dev.frak.id`)||e.includes(`wallet.gcp-dev.frak.id`)?`https://backend.gcp-dev.frak.id`:m}function _(e){if(e)return g(e);if(typeof window<`u`){let e=window.FrakSetup?.client?.config?.walletUrl;if(e)return g(e)}return m}function v(e){return(0,t.jsonDecode)(s(e))}function y(e){return`r`in e&&!(`v`in e)}function b(e){return`v`in e&&e.v===2}const x=`fCtx`;function S(t){if(t)try{return b(t)?!t.c||!t.m||!t.t?void 0:c({v:2,c:t.c,m:t.m,t:t.t}):o((0,e.hexToBytes)(t.r))}catch(e){console.error(`Error compressing Frak context`,{e,context:t})}}function C(t){if(!(!t||t.length===0))try{let n=v(t);if(n&&typeof n==`object`&&n.v===2)return n.c&&n.m&&n.t?{v:2,c:n.c,m:n.m,t:n.t}:void 0;let r=(0,e.bytesToHex)(s(t),{size:20});if((0,e.isAddress)(r))return{r}}catch(e){console.error(`Error decompressing Frak context`,{e,context:t})}}function w({url:e}){if(!e)return null;let t=new URL(e).searchParams.get(x);return t?C(t):null}function T({url:e,context:t}){if(!e)return null;let n=S(t);if(!n)return null;let r=new URL(e);return r.searchParams.set(x,n),r.toString()}function E(e){let t=new URL(e);return t.searchParams.delete(x),t.toString()}function D({url:e,context:t}){if(!window.location?.href||typeof window>`u`){console.error(`No window found, can't update context`);return}let n=e??window.location.href,r;r=t===null?E(n):T({url:n,context:t}),r&&window.history.replaceState(null,``,r.toString())}const O={compress:S,decompress:C,parse:w,update:T,remove:E,replaceUrl:D},k=`__frakSdkConfig`,A=`frak-config-cache`,j=`frak-merchant-id`,M={key:A},N=typeof window<`u`;function P(){return{isResolved:!1,merchantId:``}}let F=null;function I(){if(!N)return null;try{let e=localStorage.getItem(M.key);if(!e)return null;let t=JSON.parse(e);return t.config?.isResolved?(F=t,t):null}catch{return null}}function L(){return(F??I())?.config}function R(){let e=F??I();return e?Date.now()-e.timestamp<3e4:!1}function z(e){if(!(!N||!e.isResolved))try{let t={config:e,timestamp:Date.now()};localStorage.setItem(M.key,JSON.stringify(t)),F=t}catch{}}function B(){if(N){F=null;try{localStorage.removeItem(M.key)}catch{}}}function V(){N&&(window[k]||(window[k]=L()??P()))}V();function H(){return N?window[k]??P():P()}function U(e){N&&window.dispatchEvent(new CustomEvent(`frak:config`,{detail:e}))}function W(e){return e??(N?window.location.hostname:``)}const G=new Map,K=new Map;function q(e,t){return`${e}:${t??``}`}async function J(e,t,n){try{let r=_(t),i=n?`&lang=${encodeURIComponent(n)}`:``,a=await fetch(`${r}/user/merchant/resolve?domain=${encodeURIComponent(e)}${i}`);if(!a.ok){console.warn(`[Frak SDK] Merchant lookup failed for domain ${e}: ${a.status}`);return}let o=await a.json(),s=q(e,n);if(G.set(s,o),N)try{sessionStorage.setItem(j,o.merchantId)}catch{}return o}catch(e){console.warn(`[Frak SDK] Failed to fetch merchant config:`,e);return}}const Y={getConfig:H,get isResolved(){return H().isResolved},get isCacheFresh(){return R()},setCacheScope(e,t){M.key=`${A}:${`${e}:${t??``}`}`,F=null},setConfig(e){if(N&&(window[k]=e),z(e),U(e),N&&e.merchantId)try{sessionStorage.setItem(j,e.merchantId)}catch{}},reset(){let e=L()??P();N&&(window[k]=e),U(e)},clearCache(){if(B(),G.clear(),K.clear(),N)try{sessionStorage.removeItem(j)}catch{}},resolve(e,t,n){let r=W(e);if(!r)return Promise.resolve(void 0);let i=q(r,n);if(G.has(i))return Promise.resolve(G.get(i));let a=K.get(i);if(a)return a;let o=J(r,t,n).then(e=>(K.delete(i),e));return K.set(i,o),o},getMerchantId(){let e=H();if(e.isResolved&&e.merchantId)return e.merchantId;if(N)try{return sessionStorage.getItem(j)??void 0}catch{}},async resolveMerchantId(e,t){return Y.getMerchantId()||(await Y.resolve(e,t))?.merchantId}};function X(e,t,n={}){if(!e){console.debug(`[Frak] No client provided, skipping event tracking`);return}try{e.openPanel?.track(t,n)}catch(e){console.debug(`[Frak] Failed to track event:`,t,e)}}Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return b}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,`d`,{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,`f`,{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,`g`,{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,`h`,{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return y}}),Object.defineProperty(exports,`l`,{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,`m`,{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return Y}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return v}}),Object.defineProperty(exports,`p`,{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return O}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return _}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return X}}),Object.defineProperty(exports,`u`,{enumerable:!0,get:function(){return f}});
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "url": "https://twitter.com/QNivelais"
12
12
  }
13
13
  ],
14
- "version": "0.2.0",
14
+ "version": "0.2.1-beta.06c52c98",
15
15
  "description": "Core SDK of the Frak wallet, low level library to interact directly with the frak ecosystem.",
16
16
  "repository": {
17
17
  "url": "https://github.com/frak-id/wallet",
@@ -91,22 +91,22 @@
91
91
  "viem": "^2.x"
92
92
  },
93
93
  "dependencies": {
94
- "@frak-labs/frame-connector": "0.2.0",
95
- "@openpanel/web": "^1.0.7"
94
+ "@frak-labs/frame-connector": "0.2.0-beta.06c52c98",
95
+ "@openpanel/web": "^1.2.0"
96
96
  },
97
97
  "devDependencies": {
98
98
  "@arethetypeswrong/cli": "^0.18.2",
99
99
  "@frak-labs/dev-tooling": "0.0.0",
100
100
  "@frak-labs/test-foundation": "0.1.0",
101
101
  "@rolldown/plugin-node-polyfills": "^1.0.3",
102
- "@types/jsdom": "^27.0.0",
102
+ "@types/jsdom": "^28.0.0",
103
103
  "@types/node": "^24.10.13",
104
- "@vitest/coverage-v8": "^4.0.18",
105
- "@vitest/ui": "^4.0.18",
106
- "jsdom": "^28.0.0",
104
+ "@vitest/coverage-v8": "^4.1.0",
105
+ "@vitest/ui": "^4.1.0",
106
+ "jsdom": "^29.0.0",
107
107
  "tsdown": "^0.20.3",
108
108
  "typescript": "^5.9.3",
109
109
  "viem": "^2.39.0",
110
- "vitest": "^4.0.18"
110
+ "vitest": "^4.1.0"
111
111
  }
112
112
  }
@@ -8,14 +8,18 @@ import type {
8
8
  * Function used to display the Frak embedded wallet popup
9
9
  * @param client - The current Frak Client
10
10
  * @param params - The parameter used to customise the embedded wallet
11
+ * @param placement - Optional placement ID to associate with this display request
11
12
  * @returns The embedded wallet display result
12
13
  */
13
14
  export async function displayEmbeddedWallet(
14
15
  client: FrakClient,
15
- params: DisplayEmbeddedWalletParamsType
16
+ params: DisplayEmbeddedWalletParamsType,
17
+ placement?: string
16
18
  ): Promise<DisplayEmbeddedWalletResultType> {
17
19
  return await client.request({
18
20
  method: "frak_displayEmbeddedWallet",
19
- params: [params, client.config.metadata],
21
+ params: placement
22
+ ? [params, client.config.metadata, placement]
23
+ : [params, client.config.metadata],
20
24
  });
21
25
  }
@@ -11,6 +11,7 @@ import type {
11
11
  * @param args
12
12
  * @param args.steps - The different steps of the modal
13
13
  * @param args.metadata - The metadata for the modal (customization, etc)
14
+ * @param placement - Optional placement ID to associate with this modal display
14
15
  * @returns The result of each modal steps
15
16
  *
16
17
  * @description This function will display a modal to the user with the provided steps and metadata.
@@ -111,10 +112,13 @@ export async function displayModal<
111
112
  T extends ModalStepTypes[] = ModalStepTypes[],
112
113
  >(
113
114
  client: FrakClient,
114
- { steps, metadata }: DisplayModalParamsType<T>
115
+ { steps, metadata }: DisplayModalParamsType<T>,
116
+ placement?: string
115
117
  ): Promise<ModalRpcStepsResultType<T>> {
116
118
  return (await client.request({
117
119
  method: "frak_displayModal",
118
- params: [steps, metadata, client.config.metadata],
120
+ params: placement
121
+ ? [steps, metadata, client.config.metadata, placement]
122
+ : [steps, metadata, client.config.metadata],
119
123
  })) as ModalRpcStepsResultType<T>;
120
124
  }
@@ -1,6 +1,6 @@
1
1
  import { getBackendUrl } from "../utils/backendUrl";
2
2
  import { getClientId } from "../utils/clientId";
3
- import { fetchMerchantId } from "../utils/merchantId";
3
+ import { sdkConfigStore } from "../utils/sdkConfigStore";
4
4
 
5
5
  const ENSURE_STORAGE_PREFIX = "frak-identity-ensured-";
6
6
 
@@ -36,7 +36,7 @@ export async function ensureIdentity(interactionToken: string): Promise<void> {
36
36
  return;
37
37
  }
38
38
 
39
- const merchantId = await fetchMerchantId();
39
+ const merchantId = await sdkConfigStore.resolveMerchantId();
40
40
  if (!merchantId) {
41
41
  return;
42
42
  }
@@ -1,6 +1,5 @@
1
- import { FrakRpcError, RpcErrorCodes } from "@frak-labs/frame-connector";
2
1
  import type { Address } from "viem";
3
- import { vi } from "vitest"; // Keep vi from vitest for vi.mock() hoisting
2
+ import { vi } from "vitest";
4
3
  import {
5
4
  afterEach,
6
5
  beforeEach,
@@ -11,15 +10,11 @@ import {
11
10
  import type {
12
11
  FrakClient,
13
12
  FrakContext,
13
+ FrakContextV2,
14
14
  WalletStatusReturnType,
15
15
  } from "../../types";
16
16
  import { processReferral } from "./processReferral";
17
17
 
18
- // Mock dependencies
19
- vi.mock("../displayEmbeddedWallet", () => ({
20
- displayEmbeddedWallet: vi.fn(),
21
- }));
22
-
23
18
  vi.mock("../sendInteraction", () => ({
24
19
  sendInteraction: vi.fn().mockResolvedValue(undefined),
25
20
  }));
@@ -30,21 +25,18 @@ vi.mock("../../utils", () => ({
30
25
  },
31
26
  trackEvent: vi.fn(),
32
27
  resolveMerchantId: vi.fn().mockResolvedValue(undefined),
28
+ getClientId: vi.fn().mockReturnValue("test-client-id"),
33
29
  }));
34
30
 
35
31
  describe("processReferral", () => {
36
32
  let mockClient: FrakClient;
37
33
  let mockAddress: Address;
38
- let mockReferrerAddress: Address;
39
34
  let mockWalletStatus: WalletStatusReturnType;
40
- let mockFrakContext: Partial<FrakContext>;
41
35
 
42
36
  beforeEach(async () => {
43
37
  vi.clearAllMocks();
44
38
 
45
39
  mockAddress = "0x1234567890123456789012345678901234567890" as Address;
46
- mockReferrerAddress =
47
- "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" as Address;
48
40
 
49
41
  mockClient = {
50
42
  openPanel: {
@@ -64,11 +56,6 @@ describe("processReferral", () => {
64
56
  wallet: mockAddress,
65
57
  };
66
58
 
67
- mockFrakContext = {
68
- r: mockReferrerAddress,
69
- };
70
-
71
- // Mock window.location
72
59
  Object.defineProperty(window, "location", {
73
60
  value: {
74
61
  href: "https://example.com/test",
@@ -81,15 +68,6 @@ describe("processReferral", () => {
81
68
  vi.clearAllMocks();
82
69
  });
83
70
 
84
- it("should return 'no-referrer' when frakContext has no referrer", async () => {
85
- const result = await processReferral(mockClient, {
86
- walletStatus: mockWalletStatus,
87
- frakContext: {},
88
- });
89
-
90
- expect(result).toBe("no-referrer");
91
- });
92
-
93
71
  it("should return 'no-referrer' when frakContext is null", async () => {
94
72
  const result = await processReferral(mockClient, {
95
73
  walletStatus: mockWalletStatus,
@@ -99,128 +77,148 @@ describe("processReferral", () => {
99
77
  expect(result).toBe("no-referrer");
100
78
  });
101
79
 
102
- it("should return 'self-referral' when referrer equals current wallet", async () => {
103
- const result = await processReferral(mockClient, {
104
- walletStatus: mockWalletStatus,
105
- frakContext: {
106
- r: mockAddress, // Same as wallet
107
- },
108
- });
109
-
110
- expect(result).toBe("self-referral");
111
- });
112
-
113
- it("should successfully process referral when all conditions are met", async () => {
114
- const utils = await import("../../utils");
80
+ describe("V2 context", () => {
81
+ const v2Context: FrakContextV2 = {
82
+ v: 2,
83
+ c: "referrer-client-id",
84
+ m: "merchant-uuid",
85
+ t: 1709654400,
86
+ };
115
87
 
116
- const result = await processReferral(mockClient, {
117
- walletStatus: mockWalletStatus,
118
- frakContext: mockFrakContext,
88
+ it("should successfully process v2 referral", async () => {
89
+ const utils = await import("../../utils");
90
+ const { sendInteraction } = await import("../sendInteraction");
91
+
92
+ const result = await processReferral(mockClient, {
93
+ walletStatus: mockWalletStatus,
94
+ frakContext: v2Context,
95
+ });
96
+
97
+ expect(result).toBe("success");
98
+
99
+ expect(utils.trackEvent).toHaveBeenCalledWith(
100
+ mockClient,
101
+ "user_referred_started",
102
+ {
103
+ properties: {
104
+ referrerClientId: "referrer-client-id",
105
+ walletStatus: "connected",
106
+ },
107
+ }
108
+ );
109
+
110
+ expect(sendInteraction).toHaveBeenCalledWith(mockClient, {
111
+ type: "arrival",
112
+ referrerClientId: "referrer-client-id",
113
+ referrerMerchantId: "merchant-uuid",
114
+ referralTimestamp: 1709654400,
115
+ landingUrl: "https://example.com/test",
116
+ });
119
117
  });
120
118
 
121
- expect(result).toBe("success");
119
+ it("should return 'self-referral' when v2 context has same clientId as current user", async () => {
120
+ const utils = await import("../../utils");
121
+ vi.mocked(utils.getClientId).mockReturnValue("referrer-client-id");
122
122
 
123
- expect(utils.trackEvent).toHaveBeenCalledWith(
124
- mockClient,
125
- "user_referred_started",
126
- {
127
- properties: {
128
- referrer: mockReferrerAddress,
129
- walletStatus: "connected",
130
- },
131
- }
132
- );
133
-
134
- expect(utils.trackEvent).toHaveBeenCalledWith(
135
- mockClient,
136
- "user_referred_completed",
137
- {
138
- properties: {
139
- status: "success",
140
- referrer: mockReferrerAddress,
141
- wallet: mockAddress,
142
- },
143
- }
144
- );
145
- });
123
+ const v2SelfReferralContext: FrakContextV2 = {
124
+ v: 2,
125
+ c: "referrer-client-id",
126
+ m: "merchant-uuid",
127
+ t: 1709654400,
128
+ };
146
129
 
147
- it("should handle wallet not connected scenario", async () => {
148
- const { displayEmbeddedWallet } = await import(
149
- "../displayEmbeddedWallet"
150
- );
130
+ const result = await processReferral(mockClient, {
131
+ walletStatus: mockWalletStatus,
132
+ frakContext: v2SelfReferralContext,
133
+ });
151
134
 
152
- // Mock displayEmbeddedWallet to return a wallet
153
- vi.mocked(displayEmbeddedWallet).mockResolvedValue({
154
- wallet: mockAddress,
155
- } as any);
156
-
157
- const result = await processReferral(mockClient, {
158
- walletStatus: undefined,
159
- frakContext: mockFrakContext,
135
+ expect(result).toBe("self-referral");
136
+ vi.mocked(utils.getClientId).mockReturnValue("test-client-id");
160
137
  });
161
-
162
- expect(result).toBe("success");
163
- expect(displayEmbeddedWallet).toHaveBeenCalled();
164
138
  });
165
139
 
166
- it("should return 'no-wallet' when wallet connection fails", async () => {
167
- const { displayEmbeddedWallet } = await import(
168
- "../displayEmbeddedWallet"
169
- );
170
-
171
- const error = new FrakRpcError(
172
- RpcErrorCodes.walletNotConnected,
173
- "Wallet not connected"
174
- );
175
- vi.mocked(displayEmbeddedWallet).mockRejectedValue(error);
140
+ describe("V1 context (backward compat)", () => {
141
+ const v1Context: FrakContext = {
142
+ r: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" as Address,
143
+ };
176
144
 
177
- const result = await processReferral(mockClient, {
178
- walletStatus: undefined,
179
- frakContext: mockFrakContext,
145
+ it("should successfully process v1 referral", async () => {
146
+ const utils = await import("../../utils");
147
+
148
+ const result = await processReferral(mockClient, {
149
+ walletStatus: mockWalletStatus,
150
+ frakContext: v1Context,
151
+ });
152
+
153
+ expect(result).toBe("success");
154
+
155
+ expect(utils.trackEvent).toHaveBeenCalledWith(
156
+ mockClient,
157
+ "user_referred_started",
158
+ {
159
+ properties: {
160
+ referrer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
161
+ walletStatus: "connected",
162
+ },
163
+ }
164
+ );
180
165
  });
181
166
 
182
- expect(result).toBe("no-wallet");
183
- });
184
-
185
- it("should return 'error' for unknown errors", async () => {
186
- const { displayEmbeddedWallet } = await import(
187
- "../displayEmbeddedWallet"
188
- );
189
-
190
- const error = new Error("Unknown error");
191
- vi.mocked(displayEmbeddedWallet).mockRejectedValue(error);
167
+ it("should return 'self-referral' when v1 referrer matches current wallet", async () => {
168
+ const result = await processReferral(mockClient, {
169
+ walletStatus: mockWalletStatus,
170
+ frakContext: {
171
+ r: mockAddress,
172
+ },
173
+ });
192
174
 
193
- const result = await processReferral(mockClient, {
194
- walletStatus: undefined,
195
- frakContext: mockFrakContext,
175
+ expect(result).toBe("self-referral");
196
176
  });
197
-
198
- expect(result).toBe("error");
199
177
  });
200
178
 
201
179
  it("should update URL context when alwaysAppendUrl is true", async () => {
202
180
  const utils = await import("../../utils");
203
181
 
182
+ const v2Context: FrakContextV2 = {
183
+ v: 2,
184
+ c: "referrer-client-id",
185
+ m: "merchant-uuid",
186
+ t: 1709654400,
187
+ };
188
+
204
189
  await processReferral(mockClient, {
205
190
  walletStatus: mockWalletStatus,
206
- frakContext: mockFrakContext,
191
+ frakContext: v2Context,
207
192
  options: {
208
193
  alwaysAppendUrl: true,
209
194
  },
210
195
  });
211
196
 
197
+ expect(utils.getClientId()).toBe("test-client-id");
198
+
212
199
  expect(utils.FrakContextManager.replaceUrl).toHaveBeenCalledWith({
213
200
  url: window.location.href,
214
- context: { r: mockAddress },
201
+ context: expect.objectContaining({
202
+ v: 2,
203
+ c: "test-client-id",
204
+ m: "merchant-uuid",
205
+ }),
215
206
  });
216
207
  });
217
208
 
218
209
  it("should remove URL context when alwaysAppendUrl is false", async () => {
219
210
  const utils = await import("../../utils");
220
211
 
212
+ const v2Context: FrakContextV2 = {
213
+ v: 2,
214
+ c: "referrer-client-id",
215
+ m: "merchant-uuid",
216
+ t: 1709654400,
217
+ };
218
+
221
219
  await processReferral(mockClient, {
222
220
  walletStatus: mockWalletStatus,
223
- frakContext: mockFrakContext,
221
+ frakContext: v2Context,
224
222
  options: {
225
223
  alwaysAppendUrl: false,
226
224
  },
@@ -231,18 +229,4 @@ describe("processReferral", () => {
231
229
  context: null,
232
230
  });
233
231
  });
234
-
235
- it("should remove URL context by default", async () => {
236
- const utils = await import("../../utils");
237
-
238
- await processReferral(mockClient, {
239
- walletStatus: mockWalletStatus,
240
- frakContext: mockFrakContext,
241
- });
242
-
243
- expect(utils.FrakContextManager.replaceUrl).toHaveBeenCalledWith({
244
- url: window.location.href,
245
- context: null,
246
- });
247
- });
248
232
  });