@frak-labs/core-sdk 0.2.1 → 1.0.0-beta.61e6fb99

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 (92) hide show
  1. package/README.md +1 -2
  2. package/cdn/bundle.js +3 -3
  3. package/dist/actions-Di4welXI.cjs +1 -0
  4. package/dist/actions-DyMkUe65.js +1 -0
  5. package/dist/actions.cjs +1 -1
  6. package/dist/actions.d.cts +3 -3
  7. package/dist/actions.d.ts +3 -3
  8. package/dist/actions.js +1 -1
  9. package/dist/bundle.cjs +1 -1
  10. package/dist/bundle.d.cts +4 -4
  11. package/dist/bundle.d.ts +4 -4
  12. package/dist/bundle.js +1 -1
  13. package/dist/{computeLegacyProductId-CCAZvLa5.d.cts → index-B_Uj-puh.d.ts} +249 -73
  14. package/dist/{computeLegacyProductId-b5cUWdAm.d.ts → index-ByVpu25D.d.cts} +249 -73
  15. package/dist/{siweAuthenticate-CnCZ7mok.d.ts → index-CGyEOo9J.d.cts} +122 -8
  16. package/dist/{siweAuthenticate-CVigMOxz.d.cts → index-Cdf5j2_W.d.ts} +122 -8
  17. package/dist/index.cjs +1 -1
  18. package/dist/index.d.cts +3 -3
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +1 -1
  21. package/dist/{openSso-B0g7-807.d.cts → openSso-B6pD2oA6.d.ts} +380 -46
  22. package/dist/{openSso-CMzwvaCa.d.ts → openSso-qjaccFd0.d.cts} +379 -45
  23. package/dist/sdkConfigStore-DvwFc6Ym.cjs +1 -0
  24. package/dist/sdkConfigStore-M37skmM8.js +1 -0
  25. package/dist/src-BqpqVHCq.cjs +13 -0
  26. package/dist/src-BxRYON49.js +13 -0
  27. package/package.json +12 -13
  28. package/src/actions/displayEmbeddedWallet.ts +6 -2
  29. package/src/actions/displayModal.ts +6 -2
  30. package/src/actions/displaySharingPage.ts +49 -0
  31. package/src/actions/ensureIdentity.ts +2 -2
  32. package/src/actions/getMerchantInformation.test.ts +13 -1
  33. package/src/actions/getMerchantInformation.ts +20 -5
  34. package/src/actions/getMergeToken.ts +33 -0
  35. package/src/actions/getUserReferralStatus.ts +42 -0
  36. package/src/actions/index.ts +8 -1
  37. package/src/actions/referral/processReferral.test.ts +4 -8
  38. package/src/actions/referral/processReferral.ts +5 -11
  39. package/src/actions/referral/setupReferral.test.ts +79 -0
  40. package/src/actions/referral/setupReferral.ts +32 -0
  41. package/src/actions/trackPurchaseStatus.test.ts +32 -20
  42. package/src/actions/trackPurchaseStatus.ts +3 -5
  43. package/src/actions/wrapper/modalBuilder.test.ts +4 -2
  44. package/src/actions/wrapper/modalBuilder.ts +6 -8
  45. package/src/clients/createIFrameFrakClient.ts +233 -28
  46. package/src/clients/transports/iframeLifecycleManager.test.ts +14 -94
  47. package/src/clients/transports/iframeLifecycleManager.ts +35 -53
  48. package/src/index.ts +25 -5
  49. package/src/stubs/rrweb.ts +9 -0
  50. package/src/types/config.ts +19 -3
  51. package/src/types/index.ts +15 -1
  52. package/src/types/lifecycle/client.ts +29 -27
  53. package/src/types/lifecycle/iframe.ts +7 -8
  54. package/src/types/resolvedConfig.ts +138 -0
  55. package/src/types/rpc/displaySharingPage.ts +100 -0
  56. package/src/types/rpc/embedded/index.ts +1 -1
  57. package/src/types/rpc/interaction.ts +4 -0
  58. package/src/types/rpc/userReferralStatus.ts +20 -0
  59. package/src/types/rpc.ts +54 -5
  60. package/src/types/tracking.ts +36 -0
  61. package/src/utils/FrakContext.test.ts +151 -0
  62. package/src/utils/FrakContext.ts +67 -1
  63. package/src/utils/analytics/events/component.ts +58 -0
  64. package/src/utils/analytics/events/index.ts +20 -0
  65. package/src/utils/analytics/events/lifecycle.ts +26 -0
  66. package/src/utils/analytics/events/referral.ts +10 -0
  67. package/src/utils/analytics/index.ts +8 -0
  68. package/src/utils/{trackEvent.test.ts → analytics/trackEvent.test.ts} +22 -30
  69. package/src/utils/analytics/trackEvent.ts +34 -0
  70. package/src/utils/backendUrl.test.ts +2 -2
  71. package/src/utils/backendUrl.ts +1 -1
  72. package/src/utils/cache/index.ts +7 -0
  73. package/src/utils/cache/lruMap.test.ts +55 -0
  74. package/src/utils/cache/lruMap.ts +38 -0
  75. package/src/utils/cache/withCache.test.ts +168 -0
  76. package/src/utils/cache/withCache.ts +124 -0
  77. package/src/utils/inAppBrowser.ts +60 -0
  78. package/src/utils/index.ts +11 -5
  79. package/src/utils/mergeAttribution.test.ts +153 -0
  80. package/src/utils/mergeAttribution.ts +75 -0
  81. package/src/utils/sdkConfigStore.test.ts +405 -0
  82. package/src/utils/sdkConfigStore.ts +263 -0
  83. package/src/utils/sso.ts +3 -7
  84. package/dist/setupClient-BduY6Sym.cjs +0 -13
  85. package/dist/setupClient-ftmdQ-I8.js +0 -13
  86. package/dist/siweAuthenticate-BWmI2_TN.cjs +0 -1
  87. package/dist/siweAuthenticate-zczqxm0a.js +0 -1
  88. package/dist/trackEvent-CeLFVzZn.js +0 -1
  89. package/dist/trackEvent-Ew5r5zfI.cjs +0 -1
  90. package/src/utils/merchantId.test.ts +0 -653
  91. package/src/utils/merchantId.ts +0 -143
  92. package/src/utils/trackEvent.ts +0 -41
@@ -0,0 +1,13 @@
1
+ import{o as e,t,y as n}from"./sdkConfigStore-M37skmM8.js";import{Deferred as r,FrakRpcError as i,RpcErrorCodes as a,createRpcClient as o}from"@frak-labs/frame-connector";import{OpenPanel as s}from"@openpanel/web";const c=`nexus-wallet-backup`,l=`frakwallet://`;function u(){let e=navigator.userAgent;return/Android/i.test(e)&&/Chrome\/\d+/i.test(e)}function d(e){return`intent://${e.slice(13)}#Intent;scheme=frakwallet;end`}function f(e,t){let n=t?.timeout??2500,r=!1,i=()=>{document.hidden&&(r=!0)};document.addEventListener(`visibilitychange`,i);let a=u()&&p(e)?d(e):e;window.location.href=a,setTimeout(()=>{document.removeEventListener(`visibilitychange`,i),r||t?.onFallback?.()},n)}function p(e){return e.startsWith(l)}const m={eur:`fr-FR`,usd:`en-US`,gbp:`en-GB`};function h(e){return e&&e in m?e:`eur`}function g(e){return e?m[e]??m.eur:m.eur}function _(e,t){let n=g(t),r=h(t);return e.toLocaleString(n,{style:`currency`,currency:r,minimumFractionDigits:0,maximumFractionDigits:2})}function v(e){return e?`${e}Amount`:`eurAmount`}const y={id:`frak-wallet`,name:`frak-wallet`,title:`Frak Wallet`,allow:`publickey-credentials-get *; clipboard-write; web-share *`,style:{width:`0`,height:`0`,border:`0`,position:`absolute`,zIndex:2000001,top:`-1000px`,left:`-1000px`,colorScheme:`auto`}};function b({walletBaseUrl:e,config:t}){let r=document.querySelector(`#frak-wallet`);r&&r.remove();let i=document.createElement(`iframe`);i.id=y.id,i.name=y.name,i.allow=y.allow,i.style.zIndex=y.style.zIndex.toString(),x({iframe:i,isVisible:!1});let a=t?.walletUrl??e??`https://wallet.frak.id`,o=n();return i.src=`${a}/listener?clientId=${encodeURIComponent(o)}`,new Promise(e=>{i.addEventListener(`load`,()=>e(i)),document.body.appendChild(i)})}function x({iframe:e,isVisible:t}){if(!t){e.style.width=`0`,e.style.height=`0`,e.style.border=`0`,e.style.position=`fixed`,e.style.top=`-1000px`,e.style.left=`-1000px`;return}e.style.position=`fixed`,e.style.top=`0`,e.style.left=`0`,e.style.width=`100%`,e.style.height=`100%`,e.style.pointerEvents=`auto`}function S(e=`/listener`){if(!window.opener)return null;let t=t=>{try{return t.location.origin===window.location.origin&&t.location.pathname===e}catch{return!1}};if(t(window.opener))return window.opener;try{let e=window.opener.frames;for(let n=0;n<e.length;n++)if(t(e[n]))return e[n];return null}catch(t){return console.error(`[findIframeInOpener] Error finding iframe with pathname ${e}:`,t),null}}function C(){if(typeof navigator>`u`)return!1;let e=navigator.userAgent;return!!(/iPhone|iPad|iPod/i.test(e)||/Macintosh/i.test(e)&&navigator.maxTouchPoints>1)}const w=C();function T(){if(typeof navigator>`u`)return!1;let e=navigator.userAgent.toLowerCase();return e.includes(`instagram`)||e.includes(`fban`)||e.includes(`fbav`)||e.includes(`facebook`)}const E=T();function D(e){w&&e.startsWith(`https://`)?window.location.href=`x-safari-https://${e.slice(8)}`:w&&e.startsWith(`http://`)?window.location.href=`x-safari-http://${e.slice(7)}`:window.location.href=`https://backend.frak.id/common/social?u=${encodeURIComponent(e)}`}function O({perCall:e,defaults:t,productUtmContent:n}){if(e===null)return;let r=e!==void 0,i=t!==void 0&&Object.keys(t).length>0;if(!r&&!i&&!(n!==void 0&&n!==``))return;let a={...t,...e??{}},o=n??e?.utmContent;return o!==void 0&&o!==``?a.utmContent=o:delete a.utmContent,a}function k(e,t){if(typeof window>`u`)return;let n=new URL(window.location.href),r=n.searchParams.get(`sso`);r&&(t.then(()=>{e.sendLifecycle({clientLifecycle:`sso-redirect-complete`,data:{compressed:r}}),console.log(`[SSO URL Listener] Forwarded compressed SSO data to iframe`)}).catch(e=>{console.error(`[SSO URL Listener] Failed to forward SSO data:`,e)}),n.searchParams.delete(`sso`),window.history.replaceState({},``,n.toString()),console.log(`[SSO URL Listener] SSO parameter detected and URL cleaned`))}var A=class e{config;iframe;isSetupDone=!1;lastResponse=null;lastRequest=null;constructor(e,t){this.config=e,this.iframe=t,this.lastRequest=null,this.lastResponse=null}setLastResponse(e,t){this.lastResponse={message:e,response:t,timestamp:Date.now()}}setLastRequest(e){this.lastRequest={event:e,timestamp:Date.now()}}updateSetupStatus(e){this.isSetupDone=e}base64Encode(e){try{return btoa(JSON.stringify(e))}catch(e){return console.warn(`Failed to encode debug data`,e),btoa(`Failed to encode data`)}}getIframeStatus(){return this.iframe?{loading:this.iframe.hasAttribute(`loading`),url:this.iframe.src,readyState:this.iframe.contentDocument?.readyState?+(this.iframe.contentDocument.readyState===`complete`):-1,contentWindow:!!this.iframe.contentWindow,isConnected:this.iframe.isConnected}:null}getNavigatorInfo(){return navigator?{userAgent:navigator.userAgent,language:navigator.language,onLine:navigator.onLine,screenWidth:window.screen.width,screenHeight:window.screen.height,pixelRatio:window.devicePixelRatio}:null}gatherDebugInfo(e){let t=this.getIframeStatus(),n=this.getNavigatorInfo(),r=`Unknown`;return e instanceof i?r=`FrakRpcError: ${e.code} '${e.message}'`:e instanceof Error?r=e.message:typeof e==`string`&&(r=e),{timestamp:new Date().toISOString(),encodedUrl:btoa(window.location.href),encodedConfig:this.config?this.base64Encode(this.config):`no-config`,navigatorInfo:n?this.base64Encode(n):`no-navigator`,iframeStatus:t?this.base64Encode(t):`not-iframe`,lastRequest:this.lastRequest?this.base64Encode(this.lastRequest):`No Frak request logged`,lastResponse:this.lastResponse?this.base64Encode(this.lastResponse):`No Frak response logged`,clientStatus:this.isSetupDone?`setup`:`not-setup`,error:r}}static empty(){return new e}formatDebugInfo(e){let t=this.gatherDebugInfo(e);return`
2
+ Debug Information:
3
+ -----------------
4
+ Timestamp: ${t.timestamp}
5
+ URL: ${t.encodedUrl}
6
+ Config: ${t.encodedConfig}
7
+ Navigator Info: ${t.navigatorInfo}
8
+ IFrame Status: ${t.iframeStatus}
9
+ Last Request: ${t.lastRequest}
10
+ Last Response: ${t.lastResponse}
11
+ Client Status: ${t.clientStatus}
12
+ Error: ${t.error}
13
+ `.trim()}};const j=(()=>{if(typeof navigator>`u`)return!1;let e=navigator.userAgent;if(!(/iPhone|iPad|iPod/i.test(e)||/Macintosh/i.test(e)&&navigator.maxTouchPoints>1))return!1;let t=e.toLowerCase();return t.includes(`instagram`)||t.includes(`fban`)||t.includes(`fbav`)||t.includes(`facebook`)})();function M(e){e?localStorage.setItem(c,e):localStorage.removeItem(c)}function N(e,t){try{let n=new URL(e);if(!n.searchParams.has(`u`))return e;let r=I(window.location.href,t);return n.searchParams.delete(`u`),n.searchParams.append(`u`,r),n.toString()}catch{return e}}function P(e){let t=new URL(window.location.href);e&&t.searchParams.set(`fmt`,e);let n=t.protocol===`http:`?`x-safari-http`:`x-safari-https`;window.location.href=`${n}://${t.host}${t.pathname}${t.search}${t.hash}`}function F(e){return e.includes(`/common/social`)}function I(e,t){if(!t)return e;try{let n=new URL(e);return n.searchParams.set(`fmt`,t),n.toString()}catch{return`${e}${e.includes(`?`)?`&`:`?`}fmt=${encodeURIComponent(t)}`}}function L(e,t,n,r,i){if(i){let e=N(t,r);window.open(e,`_blank`);return}if(p(t)){let i=N(t,r);f(i,{onFallback:()=>{e.contentWindow?.postMessage({clientLifecycle:`deep-link-failed`,data:{originalUrl:i}},n)}})}else if(j&&F(t))P(r);else{let e=N(t,r);window.location.href=e}}function R({iframe:e,targetOrigin:t}){let n=new r;return{handleEvent:r=>{if(!(`iframeLifecycle`in r))return;let{iframeLifecycle:i,data:a}=r;switch(i){case`connected`:n.resolve(!0);break;case`do-backup`:M(a.backup);break;case`remove-backup`:localStorage.removeItem(c);break;case`show`:case`hide`:x({iframe:e,isVisible:i===`show`});break;case`redirect`:L(e,a.baseRedirectUrl,t,a.mergeToken,a.openInNewTab);break}},isConnected:n.promise}}function z({config:c,iframe:l}){let u=c?.walletUrl??`https://wallet.frak.id`,d=typeof navigator<`u`?navigator.language?.split(`-`)[0]:void 0,f=c.metadata.lang??(d===`en`||d===`fr`?d:void 0),p=c.domain??(typeof window<`u`?window.location.hostname:``);t.setCacheScope(p,f),t.reset();let m=t.isCacheFresh?void 0:t.resolve(c.domain,c.walletUrl,f),h=R({iframe:l,targetOrigin:u}),g=new r,_=Date.now(),v=new A(c,l);if(!l.contentWindow)throw new i(a.configError,`The iframe does not have a content window`);let y=o({emittingTransport:l.contentWindow,listeningTransport:window,targetOrigin:u,middleware:[{async onRequest(e,t){if(!await h.isConnected)throw new i(a.clientNotConnected,`The iframe provider isn't connected yet`);return await g.promise,t}},{onRequest(e,t){return v.setLastRequest(e),t},onResponse(e,t){return v.setLastResponse(e,t),t}}],lifecycleHandlers:{iframeLifecycle:(e,t)=>{h.handleEvent(e)}}}),b=B(y,h),x=async()=>{b(),y.cleanup(),l.remove(),e(),t.clearCache(),t.reset()},S;{console.log(`[Frak SDK] Initializing OpenPanel`),S=new s({apiUrl:`https://op-api.gcp.frak.id`,clientId:`6eacc8d7-49ac-4936-95e9-81ef29449570`,trackScreenViews:!0,trackOutgoingLinks:!0,trackAttributes:!1,filter:({type:e,payload:t})=>(e!==`track`||!t?.properties||`sdkVersion`in t.properties||(t.properties={...t.properties,sdkVersion:`1.0.0`,userAnonymousClientId:n()}),!0)}),S.setGlobalProperties({sdkVersion:`1.0.0`,userAnonymousClientId:n()}),S.init(),S.track(`sdk_initialized`,{sdkVersion:`1.0.0`});let e=!1,t=setTimeout(()=>{e||(e=!0,S?.track(`sdk_iframe_handshake_failed`,{reason:`timeout`}))},3e4);h.isConnected.then(()=>{e||(e=!0,clearTimeout(t),S?.track(`sdk_iframe_connected`,{handshake_duration_ms:Date.now()-_}))}).catch(()=>{e||(e=!0,clearTimeout(t),S?.track(`sdk_iframe_handshake_failed`,{reason:`unknown`}))})}let C=V({config:c,rpcClient:y,lifecycleManager:h,configPromise:m,contextSent:g,openPanel:S}).then(()=>v.updateSetupStatus(!0)).catch(e=>{throw g.reject(e),e});return{config:c,debugInfo:v,waitForConnection:h.isConnected,waitForSetup:C,request:y.request,listenerRequest:y.listen,destroy:x,openPanel:S}}function B(e,t){let n,r,i=()=>e.sendLifecycle({clientLifecycle:`heartbeat`});async function a(){i(),n=setInterval(i,1e3),r=setTimeout(()=>{o(),console.log(`Heartbeat timeout: connection failed`)},3e4),await t.isConnected,o()}function o(){n&&clearInterval(n),r&&clearTimeout(r)}return a(),o}async function V({config:e,rpcClient:r,lifecycleManager:i,configPromise:a,contextSent:o,openPanel:s}){await i.isConnected,k(r,i.isConnected);let l=new URL(window.location.href),u=l.searchParams.get(`fmt`)??void 0;u&&(l.searchParams.delete(`fmt`),window.history.replaceState({},``,l.toString()));let d=n=>{let r=n?.merchantId??e.metadata.merchantId??``,i=n?.domain??``,a=n?.allowedDomains??[],o=n?.sdkConfig,s=o?.attribution||e.attribution?{...e.attribution,...o?.attribution}:void 0;t.setConfig(o?{isResolved:!0,merchantId:r,domain:i,allowedDomains:a,hasRawSdkConfig:!0,name:o.name??e.metadata.name,logoUrl:o.logoUrl??e.metadata.logoUrl,homepageLink:o.homepageLink??e.metadata.homepageLink,lang:o.lang??e.metadata.lang,currency:o.currency??e.metadata.currency,hidden:o.hidden,css:o.css,translations:o.translations,placements:o.placements,components:o.components,attribution:s}:{isResolved:!0,merchantId:r,domain:i,allowedDomains:a,name:e.metadata.name,logoUrl:e.metadata.logoUrl,homepageLink:e.metadata.homepageLink,lang:e.metadata.lang,currency:e.metadata.currency,attribution:s})},f=!1,p=e=>{let t=f?void 0:u;f=!0;let i=e.hasRawSdkConfig?{name:e.name,logoUrl:e.logoUrl,homepageLink:e.homepageLink,lang:e.lang,currency:e.currency,hidden:e.hidden,css:e.css,translations:e.translations,placements:e.placements,attribution:e.attribution}:e.attribution?{attribution:e.attribution}:void 0,a=n();if(s){let t=s.global??{};s.setGlobalProperties({...t,merchantId:e.merchantId,domain:e.domain??``})}r.sendLifecycle({clientLifecycle:`resolved-config`,data:{merchantId:e.merchantId,domain:e.domain??``,allowedDomains:e.allowedDomains??[],sourceUrl:window.location.href,...a&&{sdkAnonymousId:a},...t&&{pendingMergeToken:t},...i&&{sdkConfig:i}}})};t.isResolved&&(p(t.getConfig()),o.resolve()),a&&(d(await a),p(t.getConfig()),o.resolve());async function m(){let t=e.customizations?.css;t&&r.sendLifecycle({clientLifecycle:`modal-css`,data:{cssLink:t}})}async function h(){let t=e.customizations?.i18n;t&&r.sendLifecycle({clientLifecycle:`modal-i18n`,data:{i18n:t}})}async function g(){if(typeof window>`u`)return;let e=window.localStorage.getItem(c);e&&r.sendLifecycle({clientLifecycle:`restore-backup`,data:{backup:e}})}(await Promise.allSettled([m(),h(),g()])).some(e=>e.status===`rejected`)&&s?.track(`sdk_iframe_handshake_failed`,{reason:`asset_push`})}async function H({config:e}){let t=U(e),n=await b({config:t});if(!n){console.error(`Failed to create iframe`);return}let r=z({config:t,iframe:n});if(await r.waitForSetup,!await r.waitForConnection){console.error(`Failed to connect to client`);return}return r}function U(e){let t=h(e.metadata?.currency);return{...e,metadata:{...e.metadata,currency:t}}}export{p as _,w as a,l as b,y as c,v as d,_ as f,u as g,m as h,O as i,b as l,h as m,z as n,E as o,g as p,A as r,D as s,H as t,S as u,d as v,f as y};
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "url": "https://twitter.com/QNivelais"
12
12
  }
13
13
  ],
14
- "version": "0.2.1",
14
+ "version": "1.0.0-beta.61e6fb99",
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,21 @@
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.61e6fb99",
95
+ "@openpanel/web": "^1.2.0"
96
96
  },
97
97
  "devDependencies": {
98
98
  "@arethetypeswrong/cli": "^0.18.2",
99
- "@frak-labs/dev-tooling": "0.0.0",
100
99
  "@frak-labs/test-foundation": "0.1.0",
101
100
  "@rolldown/plugin-node-polyfills": "^1.0.3",
102
- "@types/jsdom": "^27.0.0",
103
- "@types/node": "^24.10.13",
104
- "@vitest/coverage-v8": "^4.0.18",
105
- "@vitest/ui": "^4.0.18",
106
- "jsdom": "^28.0.0",
107
- "tsdown": "^0.20.3",
108
- "typescript": "^5.9.3",
109
- "viem": "^2.39.0",
110
- "vitest": "^4.0.18"
101
+ "@types/jsdom": "^28.0.0",
102
+ "@types/node": "^25.6.0",
103
+ "@vitest/coverage-v8": "^4.1.4",
104
+ "@vitest/ui": "^4.1.4",
105
+ "jsdom": "^29.0.0",
106
+ "tsdown": "^0.21.8",
107
+ "typescript": "^6.0.2",
108
+ "viem": "^2.47.16",
109
+ "vitest": "^4.1.4"
111
110
  }
112
111
  }
@@ -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
  }
@@ -0,0 +1,49 @@
1
+ import type {
2
+ DisplaySharingPageParamsType,
3
+ DisplaySharingPageResultType,
4
+ FrakClient,
5
+ } from "../types";
6
+
7
+ /**
8
+ * Function used to display a sharing page
9
+ * @param client - The current Frak Client
10
+ * @param params - The parameters to customize the sharing page (products, link override, metadata)
11
+ * @param placement - Optional placement ID to associate with this display request
12
+ * @returns The result indicating the user's action (shared, copied, or dismissed)
13
+ *
14
+ * @description This function will display a full-page sharing UI to the user,
15
+ * showing product info, estimated rewards, sharing steps, FAQ, and share/copy buttons.
16
+ * The sharing link is generated from the user's wallet context + merchant info.
17
+ *
18
+ * @remarks
19
+ * - The promise resolves on the first user action (share or copy) but the page stays visible
20
+ * - The user can continue to share/copy multiple times after the initial resolution
21
+ * - Dismissing the page after a share/copy action is a no-op (promise already resolved)
22
+ * - If the user dismisses without any action, the promise resolves with `{ action: "dismissed" }`
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const result = await displaySharingPage(frakClient, {
27
+ * products: [
28
+ * {
29
+ * title: "Babies camel cuir velours bout carré",
30
+ * imageUrl: "https://example.com/product.jpg",
31
+ * },
32
+ * ],
33
+ * });
34
+ *
35
+ * console.log("User action:", result.action); // "shared" | "copied" | "dismissed"
36
+ * ```
37
+ */
38
+ export async function displaySharingPage(
39
+ client: FrakClient,
40
+ params: DisplaySharingPageParamsType,
41
+ placement?: string
42
+ ): Promise<DisplaySharingPageResultType> {
43
+ return await client.request({
44
+ method: "frak_displaySharingPage",
45
+ params: placement
46
+ ? [params, client.config.metadata, placement]
47
+ : [params, client.config.metadata],
48
+ });
49
+ }
@@ -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,9 +1,21 @@
1
1
  import type { Address, Hex } from "viem";
2
- import { describe, expect, it, vi } from "../../tests/vitest-fixtures";
2
+ import {
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ vi,
8
+ } from "../../tests/vitest-fixtures";
3
9
  import type { FrakClient, GetMerchantInformationReturnType } from "../types";
10
+ import { clearAllCache } from "../utils/cache";
4
11
  import { getMerchantInformation } from "./getMerchantInformation";
5
12
 
6
13
  describe("getMerchantInformation", () => {
14
+ // Clear cache between tests to ensure isolation
15
+ beforeEach(() => {
16
+ clearAllCache();
17
+ });
18
+
7
19
  describe("success cases", () => {
8
20
  it("should call client.request with correct method", async () => {
9
21
  const mockResponse: GetMerchantInformationReturnType = {
@@ -1,16 +1,31 @@
1
1
  import type { FrakClient, GetMerchantInformationReturnType } from "../types";
2
+ import { withCache } from "../utils/cache";
2
3
 
3
4
  /**
4
- * Fetch the current merchant information (name, rewards, tiers) from the wallet iframe
5
+ * Fetch the current merchant information (name, rewards, tiers) from the wallet iframe.
6
+ *
7
+ * Results are cached in memory for 30 seconds by default. Concurrent calls
8
+ * while a request is in-flight are deduplicated automatically.
9
+ *
5
10
  * @param client - The current Frak Client
11
+ * @param options - Optional cache configuration
12
+ * @param options.cacheTime - Time in ms to cache the result. Default: 30_000 (30s). Set to 0 to disable.
6
13
  * @returns The merchant information including available reward tiers
7
14
  *
8
15
  * @see {@link @frak-labs/core-sdk!index.GetMerchantInformationReturnType | `GetMerchantInformationReturnType`} for the return type shape
9
16
  */
10
17
  export async function getMerchantInformation(
11
- client: FrakClient
18
+ client: FrakClient,
19
+ options?: { cacheTime?: number }
12
20
  ): Promise<GetMerchantInformationReturnType> {
13
- return await client.request({
14
- method: "frak_getMerchantInformation",
15
- });
21
+ return withCache(
22
+ () =>
23
+ client.request({
24
+ method: "frak_getMerchantInformation",
25
+ }),
26
+ {
27
+ cacheKey: "frak_getMerchantInformation",
28
+ cacheTime: options?.cacheTime,
29
+ }
30
+ );
16
31
  }
@@ -0,0 +1,33 @@
1
+ import type { FrakClient } from "../types";
2
+ import { withCache } from "../utils/cache";
3
+
4
+ /**
5
+ * Fetch a merge token for the current anonymous identity.
6
+ *
7
+ * Used by in-app browser redirect flows to preserve identity
8
+ * when switching from a WebView to the system browser.
9
+ * The token is appended as `?fmt=` to the redirect URL.
10
+ *
11
+ * Results are cached in memory for 30 seconds by default. Concurrent calls
12
+ * while a request is in-flight are deduplicated automatically.
13
+ *
14
+ * @param client - The current Frak Client
15
+ * @param options - Optional cache configuration
16
+ * @param options.cacheTime - Time in ms to cache the result. Default: 30_000 (30s). Set to 0 to disable.
17
+ * @returns The merge token string, or null if unavailable
18
+ */
19
+ export async function getMergeToken(
20
+ client: FrakClient,
21
+ options?: { cacheTime?: number }
22
+ ): Promise<string | null> {
23
+ return withCache(
24
+ () =>
25
+ client.request({
26
+ method: "frak_getMergeToken",
27
+ }),
28
+ {
29
+ cacheKey: "frak_getMergeToken",
30
+ cacheTime: options?.cacheTime,
31
+ }
32
+ );
33
+ }
@@ -0,0 +1,42 @@
1
+ import type { FrakClient, UserReferralStatusType } from "../types";
2
+ import { withCache } from "../utils/cache";
3
+
4
+ /**
5
+ * Fetch the current user's referral status on the current merchant.
6
+ *
7
+ * The listener resolves the user's identity (via clientId or wallet session)
8
+ * and checks whether a referral link exists where the user is the referee.
9
+ *
10
+ * Results are cached in memory for 30 seconds by default. Concurrent calls
11
+ * while a request is in-flight are deduplicated automatically.
12
+ *
13
+ * Returns `null` when the user's identity cannot be resolved.
14
+ *
15
+ * @param client - The current Frak Client
16
+ * @param options - Optional cache configuration
17
+ * @param options.cacheTime - Time in ms to cache the result. Default: 30_000 (30s). Set to 0 to disable.
18
+ * @returns The user's referral status, or `null` if identity cannot be resolved
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const status = await getUserReferralStatus(client);
23
+ * if (status?.isReferred) {
24
+ * console.log("User was referred to this merchant");
25
+ * }
26
+ * ```
27
+ */
28
+ export async function getUserReferralStatus(
29
+ client: FrakClient,
30
+ options?: { cacheTime?: number }
31
+ ): Promise<UserReferralStatusType | null> {
32
+ return withCache(
33
+ () =>
34
+ client.request({
35
+ method: "frak_getUserReferralStatus",
36
+ }),
37
+ {
38
+ cacheKey: "frak_getUserReferralStatus",
39
+ cacheTime: options?.cacheTime,
40
+ }
41
+ );
42
+ }
@@ -1,15 +1,22 @@
1
1
  export { displayEmbeddedWallet } from "./displayEmbeddedWallet";
2
2
  export { displayModal } from "./displayModal";
3
+ export { displaySharingPage } from "./displaySharingPage";
3
4
  export { ensureIdentity } from "./ensureIdentity";
4
5
  export { getMerchantInformation } from "./getMerchantInformation";
6
+ export { getMergeToken } from "./getMergeToken";
7
+ export { getUserReferralStatus } from "./getUserReferralStatus";
5
8
  export { openSso } from "./openSso";
6
9
  export { prepareSso } from "./prepareSso";
7
10
  export {
8
11
  type ProcessReferralOptions,
9
12
  processReferral,
10
13
  } from "./referral/processReferral";
11
- // Referral interaction
14
+ // Referral
12
15
  export { referralInteraction } from "./referral/referralInteraction";
16
+ export {
17
+ REFERRAL_SUCCESS_EVENT,
18
+ setupReferral,
19
+ } from "./referral/setupReferral";
13
20
  export { sendInteraction } from "./sendInteraction";
14
21
  // Helper to track the purchase status
15
22
  export { trackPurchaseStatus } from "./trackPurchaseStatus";
@@ -100,10 +100,8 @@ describe("processReferral", () => {
100
100
  mockClient,
101
101
  "user_referred_started",
102
102
  {
103
- properties: {
104
- referrerClientId: "referrer-client-id",
105
- walletStatus: "connected",
106
- },
103
+ referrerClientId: "referrer-client-id",
104
+ walletStatus: "connected",
107
105
  }
108
106
  );
109
107
 
@@ -156,10 +154,8 @@ describe("processReferral", () => {
156
154
  mockClient,
157
155
  "user_referred_started",
158
156
  {
159
- properties: {
160
- referrer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
161
- walletStatus: "connected",
162
- },
157
+ referrer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
158
+ walletStatus: "connected",
163
159
  }
164
160
  );
165
161
  });
@@ -54,10 +54,8 @@ function trackArrivalIfValid(
54
54
 
55
55
  if (isV2Context(frakContext)) {
56
56
  trackEvent(client, "user_referred_started", {
57
- properties: {
58
- referrerClientId: frakContext.c,
59
- walletStatus: walletStatus?.key,
60
- },
57
+ referrerClientId: frakContext.c,
58
+ walletStatus: walletStatus?.key,
61
59
  });
62
60
  sendInteraction(client, {
63
61
  type: "arrival",
@@ -71,10 +69,8 @@ function trackArrivalIfValid(
71
69
 
72
70
  if (isV1Context(frakContext)) {
73
71
  trackEvent(client, "user_referred_started", {
74
- properties: {
75
- referrer: frakContext.r,
76
- walletStatus: walletStatus?.key,
77
- },
72
+ referrer: frakContext.r,
73
+ walletStatus: walletStatus?.key,
78
74
  });
79
75
  sendInteraction(client, {
80
76
  type: "arrival",
@@ -177,9 +173,7 @@ export function processReferral(
177
173
  });
178
174
 
179
175
  trackEvent(client, "user_referred_completed", {
180
- properties: {
181
- status: "success",
182
- },
176
+ status: "success",
183
177
  });
184
178
 
185
179
  return "success";
@@ -0,0 +1,79 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { REFERRAL_SUCCESS_EVENT, setupReferral } from "./setupReferral";
3
+
4
+ vi.mock("./referralInteraction", () => ({
5
+ referralInteraction: vi.fn(),
6
+ }));
7
+
8
+ import { referralInteraction } from "./referralInteraction";
9
+
10
+ describe("setupReferral", () => {
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ });
14
+
15
+ afterEach(() => {
16
+ vi.restoreAllMocks();
17
+ });
18
+
19
+ it("should dispatch referral success event on successful referral", async () => {
20
+ vi.mocked(referralInteraction).mockResolvedValue("success");
21
+ const listener = vi.fn();
22
+ window.addEventListener(REFERRAL_SUCCESS_EVENT, listener);
23
+
24
+ await setupReferral({ config: {} } as any);
25
+
26
+ expect(listener).toHaveBeenCalledTimes(1);
27
+ expect(listener).toHaveBeenCalledWith(expect.any(Event));
28
+
29
+ window.removeEventListener(REFERRAL_SUCCESS_EVENT, listener);
30
+ });
31
+
32
+ it("should not dispatch event when referral state is not success", async () => {
33
+ vi.mocked(referralInteraction).mockResolvedValue("no-referrer");
34
+ const listener = vi.fn();
35
+ window.addEventListener(REFERRAL_SUCCESS_EVENT, listener);
36
+
37
+ await setupReferral({ config: {} } as any);
38
+
39
+ expect(listener).not.toHaveBeenCalled();
40
+
41
+ window.removeEventListener(REFERRAL_SUCCESS_EVENT, listener);
42
+ });
43
+
44
+ it("should not dispatch event when referral returns undefined", async () => {
45
+ vi.mocked(referralInteraction).mockResolvedValue(undefined);
46
+ const listener = vi.fn();
47
+ window.addEventListener(REFERRAL_SUCCESS_EVENT, listener);
48
+
49
+ await setupReferral({ config: {} } as any);
50
+
51
+ expect(listener).not.toHaveBeenCalled();
52
+
53
+ window.removeEventListener(REFERRAL_SUCCESS_EVENT, listener);
54
+ });
55
+
56
+ it("should silently catch errors and log warning", async () => {
57
+ vi.mocked(referralInteraction).mockRejectedValue(
58
+ new Error("network failure")
59
+ );
60
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
61
+ const listener = vi.fn();
62
+ window.addEventListener(REFERRAL_SUCCESS_EVENT, listener);
63
+
64
+ await setupReferral({ config: {} } as any);
65
+
66
+ expect(listener).not.toHaveBeenCalled();
67
+ expect(warnSpy).toHaveBeenCalledWith(
68
+ "[Frak] Referral setup failed",
69
+ expect.any(Error)
70
+ );
71
+
72
+ window.removeEventListener(REFERRAL_SUCCESS_EVENT, listener);
73
+ warnSpy.mockRestore();
74
+ });
75
+
76
+ it("should export the correct event name constant", () => {
77
+ expect(REFERRAL_SUCCESS_EVENT).toBe("frak:referral-success");
78
+ });
79
+ });
@@ -0,0 +1,32 @@
1
+ import type { FrakClient } from "../../types";
2
+ import { referralInteraction } from "./referralInteraction";
3
+
4
+ /**
5
+ * Custom event name dispatched on successful referral processing.
6
+ *
7
+ * Fired once per page load when a valid referral context is found in the URL
8
+ * and successfully tracked. Consumers (e.g. `<frak-banner>`) listen for this
9
+ * to display a referral success message.
10
+ */
11
+ export const REFERRAL_SUCCESS_EVENT = "frak:referral-success";
12
+
13
+ /**
14
+ * Process referral context and emit a DOM event on success.
15
+ *
16
+ * - Calls {@link referralInteraction} to detect and track any referral in the URL
17
+ * - On `"success"`, dispatches a bare {@link REFERRAL_SUCCESS_EVENT} on `window`
18
+ * - Silently swallows errors (fire-and-forget during SDK init)
19
+ *
20
+ * @param client - The initialized Frak client
21
+ */
22
+ export async function setupReferral(client: FrakClient): Promise<void> {
23
+ try {
24
+ const state = await referralInteraction(client);
25
+
26
+ if (state === "success") {
27
+ window.dispatchEvent(new Event(REFERRAL_SUCCESS_EVENT));
28
+ }
29
+ } catch (error) {
30
+ console.warn("[Frak] Referral setup failed", error);
31
+ }
32
+ }
@@ -11,12 +11,14 @@ vi.mock("../utils/clientId", () => ({
11
11
  getClientId: vi.fn().mockReturnValue("test-client-id"),
12
12
  }));
13
13
 
14
- vi.mock("../utils/merchantId", () => ({
15
- fetchMerchantId: vi.fn().mockResolvedValue(undefined),
14
+ vi.mock("../utils/sdkConfigStore", () => ({
15
+ sdkConfigStore: {
16
+ resolveMerchantId: vi.fn().mockResolvedValue(undefined),
17
+ },
16
18
  }));
17
19
 
18
20
  import { getClientId } from "../utils/clientId";
19
- import { fetchMerchantId } from "../utils/merchantId";
21
+ import { sdkConfigStore } from "../utils/sdkConfigStore";
20
22
  import { trackPurchaseStatus } from "./trackPurchaseStatus";
21
23
 
22
24
  describe.sequential("trackPurchaseStatus", () => {
@@ -100,7 +102,9 @@ describe.sequential("trackPurchaseStatus", () => {
100
102
  });
101
103
 
102
104
  vi.mocked(getClientId).mockReturnValue("test-client-id");
103
- vi.mocked(fetchMerchantId).mockResolvedValue(undefined);
105
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
106
+ undefined
107
+ );
104
108
 
105
109
  fetchSpy = vi.fn().mockResolvedValue({
106
110
  ok: true,
@@ -228,12 +232,15 @@ describe.sequential("trackPurchaseStatus", () => {
228
232
  test("should resolve merchantId from explicit param first", async () => {
229
233
  setupStorage({
230
234
  interactionToken: "token-123",
231
- merchantId: "session-merchant-id",
235
+ merchantId: null,
232
236
  clientId: "test-client-id",
233
237
  });
234
- vi.mocked(fetchMerchantId).mockResolvedValue("fetched-merchant-id");
235
- const merchantLookupCallsBefore =
236
- vi.mocked(fetchMerchantId).mock.calls.length;
238
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
239
+ "fetched-merchant-id"
240
+ );
241
+ const merchantLookupCallsBefore = vi.mocked(
242
+ sdkConfigStore.resolveMerchantId
243
+ ).mock.calls.length;
237
244
 
238
245
  await trackPurchaseStatus({
239
246
  customerId: "cust-1",
@@ -253,19 +260,20 @@ describe.sequential("trackPurchaseStatus", () => {
253
260
  merchantId: "explicit-merchant-id",
254
261
  })
255
262
  );
256
- expect(vi.mocked(fetchMerchantId).mock.calls.length).toBe(
257
- merchantLookupCallsBefore
258
- );
263
+ expect(
264
+ vi.mocked(sdkConfigStore.resolveMerchantId).mock.calls.length
265
+ ).toBe(merchantLookupCallsBefore);
259
266
  });
260
267
 
261
268
  test("should fall back to sessionStorage for merchantId", async () => {
269
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
270
+ "session-merchant-id"
271
+ );
262
272
  setupStorage({
263
273
  interactionToken: "token-123",
264
- merchantId: "session-merchant-id",
274
+ merchantId: null,
265
275
  clientId: "test-client-id",
266
276
  });
267
- const merchantLookupCallsBefore =
268
- vi.mocked(fetchMerchantId).mock.calls.length;
269
277
 
270
278
  await trackPurchaseStatus({
271
279
  customerId: "cust-1",
@@ -284,18 +292,20 @@ describe.sequential("trackPurchaseStatus", () => {
284
292
  merchantId: "session-merchant-id",
285
293
  })
286
294
  );
287
- expect(vi.mocked(fetchMerchantId).mock.calls.length).toBe(
288
- merchantLookupCallsBefore
289
- );
295
+ expect(
296
+ vi.mocked(sdkConfigStore.resolveMerchantId)
297
+ ).toHaveBeenCalled();
290
298
  });
291
299
 
292
- test("should fall back to fetchMerchantId when no explicit or sessionStorage", async () => {
300
+ test("should fall back to resolveMerchantId when no explicit merchantId", async () => {
293
301
  setupStorage({
294
302
  interactionToken: "token-123",
295
303
  merchantId: null,
296
304
  clientId: "test-client-id",
297
305
  });
298
- vi.mocked(fetchMerchantId).mockResolvedValue("fetched-merchant-id");
306
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
307
+ "fetched-merchant-id"
308
+ );
299
309
 
300
310
  await trackPurchaseStatus({
301
311
  customerId: "cust-1",
@@ -322,7 +332,9 @@ describe.sequential("trackPurchaseStatus", () => {
322
332
  merchantId: null,
323
333
  clientId: "test-client-id",
324
334
  });
325
- vi.mocked(fetchMerchantId).mockResolvedValue(undefined);
335
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
336
+ undefined
337
+ );
326
338
  const callCountBefore = getTrackingRequests().length;
327
339
 
328
340
  await trackPurchaseStatus({