@frak-labs/core-sdk 0.2.1 → 1.0.0-beta.10dada3f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/cdn/bundle.js +3 -3
- package/dist/actions-Bsl5ub7_.js +1 -0
- package/dist/actions-C_B0fn1P.cjs +1 -0
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +3 -3
- package/dist/actions.d.ts +3 -3
- package/dist/actions.js +1 -1
- package/dist/bundle.cjs +1 -1
- package/dist/bundle.d.cts +4 -4
- package/dist/bundle.d.ts +4 -4
- package/dist/bundle.js +1 -1
- package/dist/{computeLegacyProductId-CCAZvLa5.d.cts → index-BSGP3dbi.d.ts} +250 -73
- package/dist/{siweAuthenticate-CVigMOxz.d.cts → index-Bh0TuKYS.d.ts} +122 -8
- package/dist/{siweAuthenticate-CnCZ7mok.d.ts → index-DW8xes2o.d.cts} +122 -8
- package/dist/{computeLegacyProductId-b5cUWdAm.d.ts → index-TRJNS6B5.d.cts} +250 -73
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/{openSso-B0g7-807.d.cts → openSso-Bvhy_urG.d.cts} +395 -50
- package/dist/{openSso-CMzwvaCa.d.ts → openSso-D2kTUv0-.d.ts} +394 -49
- package/dist/sdkConfigStore-B6CkorsU.cjs +1 -0
- package/dist/sdkConfigStore-Dx0oAVEO.js +1 -0
- package/dist/src-CLF8o8WB.cjs +13 -0
- package/dist/src-al3X6r-n.js +13 -0
- package/package.json +12 -13
- package/src/actions/displayEmbeddedWallet.ts +6 -2
- package/src/actions/displayModal.ts +6 -2
- package/src/actions/displaySharingPage.ts +49 -0
- package/src/actions/ensureIdentity.ts +2 -2
- package/src/actions/getMerchantInformation.test.ts +13 -1
- package/src/actions/getMerchantInformation.ts +20 -5
- package/src/actions/getMergeToken.ts +33 -0
- package/src/actions/getUserReferralStatus.ts +42 -0
- package/src/actions/index.ts +8 -1
- package/src/actions/referral/processReferral.test.ts +73 -8
- package/src/actions/referral/processReferral.ts +15 -12
- package/src/actions/referral/setupReferral.test.ts +79 -0
- package/src/actions/referral/setupReferral.ts +32 -0
- package/src/actions/trackPurchaseStatus.test.ts +32 -20
- package/src/actions/trackPurchaseStatus.ts +3 -5
- package/src/actions/wrapper/modalBuilder.test.ts +4 -2
- package/src/actions/wrapper/modalBuilder.ts +6 -8
- package/src/clients/createIFrameFrakClient.ts +233 -28
- package/src/clients/transports/iframeLifecycleManager.test.ts +14 -94
- package/src/clients/transports/iframeLifecycleManager.ts +35 -53
- package/src/index.ts +25 -5
- package/src/stubs/rrweb.ts +9 -0
- package/src/types/config.ts +19 -3
- package/src/types/context.ts +16 -4
- package/src/types/index.ts +15 -1
- package/src/types/lifecycle/client.ts +29 -27
- package/src/types/lifecycle/iframe.ts +7 -8
- package/src/types/resolvedConfig.ts +138 -0
- package/src/types/rpc/displaySharingPage.ts +100 -0
- package/src/types/rpc/embedded/index.ts +1 -1
- package/src/types/rpc/interaction.ts +4 -0
- package/src/types/rpc/userReferralStatus.ts +20 -0
- package/src/types/rpc.ts +54 -5
- package/src/types/tracking.ts +36 -0
- package/src/utils/FrakContext.test.ts +179 -1
- package/src/utils/FrakContext.ts +83 -8
- package/src/utils/analytics/events/component.ts +58 -0
- package/src/utils/analytics/events/index.ts +20 -0
- package/src/utils/analytics/events/lifecycle.ts +26 -0
- package/src/utils/analytics/events/referral.ts +11 -0
- package/src/utils/analytics/index.ts +8 -0
- package/src/utils/{trackEvent.test.ts → analytics/trackEvent.test.ts} +22 -30
- package/src/utils/analytics/trackEvent.ts +34 -0
- package/src/utils/backendUrl.test.ts +2 -2
- package/src/utils/backendUrl.ts +1 -1
- package/src/utils/cache/index.ts +7 -0
- package/src/utils/cache/lruMap.test.ts +55 -0
- package/src/utils/cache/lruMap.ts +38 -0
- package/src/utils/cache/withCache.test.ts +168 -0
- package/src/utils/cache/withCache.ts +124 -0
- package/src/utils/inAppBrowser.ts +60 -0
- package/src/utils/index.ts +11 -5
- package/src/utils/mergeAttribution.test.ts +153 -0
- package/src/utils/mergeAttribution.ts +75 -0
- package/src/utils/sdkConfigStore.test.ts +405 -0
- package/src/utils/sdkConfigStore.ts +263 -0
- package/src/utils/sso.ts +3 -7
- package/dist/setupClient-BduY6Sym.cjs +0 -13
- package/dist/setupClient-ftmdQ-I8.js +0 -13
- package/dist/siweAuthenticate-BWmI2_TN.cjs +0 -1
- package/dist/siweAuthenticate-zczqxm0a.js +0 -1
- package/dist/trackEvent-CeLFVzZn.js +0 -1
- package/dist/trackEvent-Ew5r5zfI.cjs +0 -1
- package/src/utils/merchantId.test.ts +0 -653
- package/src/utils/merchantId.ts +0 -143
- package/src/utils/trackEvent.ts +0 -41
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const e=require(`./sdkConfigStore-B6CkorsU.cjs`);let t=require(`@frak-labs/frame-connector`),n=require(`@openpanel/web`);const r=`nexus-wallet-backup`,i=`frakwallet://`;function a(){let e=navigator.userAgent;return/Android/i.test(e)&&/Chrome\/\d+/i.test(e)}function o(e){return`intent://${e.slice(13)}#Intent;scheme=frakwallet;end`}function s(e,t){let n=t?.timeout??2500,r=!1,i=()=>{document.hidden&&(r=!0)};document.addEventListener(`visibilitychange`,i);let s=a()&&c(e)?o(e):e;window.location.href=s,setTimeout(()=>{document.removeEventListener(`visibilitychange`,i),r||t?.onFallback?.()},n)}function c(e){return e.startsWith(i)}const l={eur:`fr-FR`,usd:`en-US`,gbp:`en-GB`};function u(e){return e&&e in l?e:`eur`}function d(e){return e?l[e]??l.eur:l.eur}function f(e,t){let n=d(t),r=u(t);return e.toLocaleString(n,{style:`currency`,currency:r,minimumFractionDigits:0,maximumFractionDigits:2})}function p(e){return e?`${e}Amount`:`eurAmount`}const m={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 h({walletBaseUrl:t,config:n}){let r=document.querySelector(`#frak-wallet`);r&&r.remove();let i=document.createElement(`iframe`);i.id=m.id,i.name=m.name,i.allow=m.allow,i.style.zIndex=m.style.zIndex.toString(),g({iframe:i,isVisible:!1});let a=n?.walletUrl??t??`https://wallet.frak.id`,o=e.y();return i.src=`${a}/listener?clientId=${encodeURIComponent(o)}`,new Promise(e=>{i.addEventListener(`load`,()=>e(i)),document.body.appendChild(i)})}function g({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 _(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 v(){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 y=v();function b(){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 x=b();function S(e){y&&e.startsWith(`https://`)?window.location.href=`x-safari-https://${e.slice(8)}`:y&&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 C({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 w(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 T=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 n=this.getIframeStatus(),r=this.getNavigatorInfo(),i=`Unknown`;return e instanceof t.FrakRpcError?i=`FrakRpcError: ${e.code} '${e.message}'`:e instanceof Error?i=e.message:typeof e==`string`&&(i=e),{timestamp:new Date().toISOString(),encodedUrl:btoa(window.location.href),encodedConfig:this.config?this.base64Encode(this.config):`no-config`,navigatorInfo:r?this.base64Encode(r):`no-navigator`,iframeStatus:n?this.base64Encode(n):`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:i}}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 E=(()=>{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 D(e){e?localStorage.setItem(r,e):localStorage.removeItem(r)}function O(e,t){try{let n=new URL(e);if(!n.searchParams.has(`u`))return e;let r=j(window.location.href,t);return n.searchParams.delete(`u`),n.searchParams.append(`u`,r),n.toString()}catch{return e}}function k(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 A(e){return e.includes(`/common/social`)}function j(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 M(e,t,n,r,i){if(i){let e=O(t,r);window.open(e,`_blank`);return}if(c(t)){let i=O(t,r);s(i,{onFallback:()=>{e.contentWindow?.postMessage({clientLifecycle:`deep-link-failed`,data:{originalUrl:i}},n)}})}else if(E&&A(t))k(r);else{let e=O(t,r);window.location.href=e}}function N({iframe:e,targetOrigin:n}){let i=new t.Deferred;return{handleEvent:t=>{if(!(`iframeLifecycle`in t))return;let{iframeLifecycle:a,data:o}=t;switch(a){case`connected`:i.resolve(!0);break;case`do-backup`:D(o.backup);break;case`remove-backup`:localStorage.removeItem(r);break;case`show`:case`hide`:g({iframe:e,isVisible:a===`show`});break;case`redirect`:M(e,o.baseRedirectUrl,n,o.mergeToken,o.openInNewTab);break}},isConnected:i.promise}}function P({config:r,iframe:i}){let a=r?.walletUrl??`https://wallet.frak.id`,o=typeof navigator<`u`?navigator.language?.split(`-`)[0]:void 0,s=r.metadata.lang??(o===`en`||o===`fr`?o:void 0),c=r.domain??(typeof window<`u`?window.location.hostname:``);e.t.setCacheScope(c,s),e.t.reset();let l=e.t.isCacheFresh?void 0:e.t.resolve(r.domain,r.walletUrl,s),u=N({iframe:i,targetOrigin:a}),d=new t.Deferred,f=Date.now(),p=new T(r,i);if(!i.contentWindow)throw new t.FrakRpcError(t.RpcErrorCodes.configError,`The iframe does not have a content window`);let m=(0,t.createRpcClient)({emittingTransport:i.contentWindow,listeningTransport:window,targetOrigin:a,middleware:[{async onRequest(e,n){if(!await u.isConnected)throw new t.FrakRpcError(t.RpcErrorCodes.clientNotConnected,`The iframe provider isn't connected yet`);return await d.promise,n}},{onRequest(e,t){return p.setLastRequest(e),t},onResponse(e,t){return p.setLastResponse(e,t),t}}],lifecycleHandlers:{iframeLifecycle:(e,t)=>{u.handleEvent(e)}}}),h=F(m,u),g=async()=>{h(),m.cleanup(),i.remove(),e.o(),e.t.clearCache(),e.t.reset()},_;{console.log(`[Frak SDK] Initializing OpenPanel`),_=new n.OpenPanel({apiUrl:`https://op-api.gcp.frak.id`,clientId:`6eacc8d7-49ac-4936-95e9-81ef29449570`,trackScreenViews:!0,trackOutgoingLinks:!0,trackAttributes:!1,filter:({type:t,payload:n})=>(t!==`track`||!n?.properties||`sdkVersion`in n.properties||(n.properties={...n.properties,sdkVersion:`1.0.0`,userAnonymousClientId:e.y()}),!0)}),_.setGlobalProperties({sdkVersion:`1.0.0`,userAnonymousClientId:e.y()}),_.init(),_.track(`sdk_initialized`,{sdkVersion:`1.0.0`});let t=!1,r=setTimeout(()=>{t||(t=!0,_?.track(`sdk_iframe_handshake_failed`,{reason:`timeout`}))},3e4);u.isConnected.then(()=>{t||(t=!0,clearTimeout(r),_?.track(`sdk_iframe_connected`,{handshake_duration_ms:Date.now()-f}))}).catch(()=>{t||(t=!0,clearTimeout(r),_?.track(`sdk_iframe_handshake_failed`,{reason:`unknown`}))})}let v=I({config:r,rpcClient:m,lifecycleManager:u,configPromise:l,contextSent:d,openPanel:_}).then(()=>p.updateSetupStatus(!0)).catch(e=>{throw d.reject(e),e});return{config:r,debugInfo:p,waitForConnection:u.isConnected,waitForSetup:v,request:m.request,listenerRequest:m.listen,destroy:g,openPanel:_}}function F(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 I({config:t,rpcClient:n,lifecycleManager:i,configPromise:a,contextSent:o,openPanel:s}){await i.isConnected,w(n,i.isConnected);let c=new URL(window.location.href),l=c.searchParams.get(`fmt`)??void 0;l&&(c.searchParams.delete(`fmt`),window.history.replaceState({},``,c.toString()));let u=n=>{let r=n?.merchantId??t.metadata.merchantId??``,i=n?.domain??``,a=n?.allowedDomains??[],o=n?.sdkConfig,s=o?.attribution||t.attribution?{...t.attribution,...o?.attribution}:void 0;e.t.setConfig(o?{isResolved:!0,merchantId:r,domain:i,allowedDomains:a,hasRawSdkConfig:!0,name:o.name??t.metadata.name,logoUrl:o.logoUrl??t.metadata.logoUrl,homepageLink:o.homepageLink??t.metadata.homepageLink,lang:o.lang??t.metadata.lang,currency:o.currency??t.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:t.metadata.name,logoUrl:t.metadata.logoUrl,homepageLink:t.metadata.homepageLink,lang:t.metadata.lang,currency:t.metadata.currency,attribution:s})},d=!1,f=t=>{let r=d?void 0:l;d=!0;let i=t.hasRawSdkConfig?{name:t.name,logoUrl:t.logoUrl,homepageLink:t.homepageLink,lang:t.lang,currency:t.currency,hidden:t.hidden,css:t.css,translations:t.translations,placements:t.placements,attribution:t.attribution}:t.attribution?{attribution:t.attribution}:void 0,a=e.y();if(s){let e=s.global??{};s.setGlobalProperties({...e,merchantId:t.merchantId,domain:t.domain??``})}n.sendLifecycle({clientLifecycle:`resolved-config`,data:{merchantId:t.merchantId,domain:t.domain??``,allowedDomains:t.allowedDomains??[],sourceUrl:window.location.href,...a&&{sdkAnonymousId:a},...r&&{pendingMergeToken:r},...i&&{sdkConfig:i}}})};e.t.isResolved&&(f(e.t.getConfig()),o.resolve()),a&&(u(await a),f(e.t.getConfig()),o.resolve());async function p(){let e=t.customizations?.css;e&&n.sendLifecycle({clientLifecycle:`modal-css`,data:{cssLink:e}})}async function m(){let e=t.customizations?.i18n;e&&n.sendLifecycle({clientLifecycle:`modal-i18n`,data:{i18n:e}})}async function h(){if(typeof window>`u`)return;let e=window.localStorage.getItem(r);e&&n.sendLifecycle({clientLifecycle:`restore-backup`,data:{backup:e}})}(await Promise.allSettled([p(),m(),h()])).some(e=>e.status===`rejected`)&&s?.track(`sdk_iframe_handshake_failed`,{reason:`asset_push`})}async function L({config:e}){let t=R(e),n=await h({config:t});if(!n){console.error(`Failed to create iframe`);return}let r=P({config:t,iframe:n});if(await r.waitForSetup,!await r.waitForConnection){console.error(`Failed to connect to client`);return}return r}function R(e){let t=u(e.metadata?.currency);return{...e,metadata:{...e.metadata,currency:t}}}Object.defineProperty(exports,`_`,{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return y}}),Object.defineProperty(exports,`b`,{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return m}}),Object.defineProperty(exports,`d`,{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,`f`,{enumerable:!0,get:function(){return f}}),Object.defineProperty(exports,`g`,{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,`h`,{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return C}}),Object.defineProperty(exports,`l`,{enumerable:!0,get:function(){return h}}),Object.defineProperty(exports,`m`,{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return P}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return x}}),Object.defineProperty(exports,`p`,{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return T}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return S}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return L}}),Object.defineProperty(exports,`u`,{enumerable:!0,get:function(){return _}}),Object.defineProperty(exports,`v`,{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,`y`,{enumerable:!0,get:function(){return s}});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{o as e,t,y as n}from"./sdkConfigStore-Dx0oAVEO.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.
|
|
14
|
+
"version": "1.0.0-beta.10dada3f",
|
|
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
|
|
94
|
+
"@frak-labs/frame-connector": "0.2.0-beta.10dada3f",
|
|
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": "^
|
|
103
|
-
"@types/node": "^
|
|
104
|
-
"@vitest/coverage-v8": "^4.
|
|
105
|
-
"@vitest/ui": "^4.
|
|
106
|
-
"jsdom": "^
|
|
107
|
-
"tsdown": "^0.
|
|
108
|
-
"typescript": "^
|
|
109
|
-
"viem": "^2.
|
|
110
|
-
"vitest": "^4.
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
14
|
-
|
|
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
|
+
}
|
package/src/actions/index.ts
CHANGED
|
@@ -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
|
|
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,9 @@ describe("processReferral", () => {
|
|
|
100
100
|
mockClient,
|
|
101
101
|
"user_referred_started",
|
|
102
102
|
{
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
},
|
|
103
|
+
referrerClientId: "referrer-client-id",
|
|
104
|
+
referrerWallet: undefined,
|
|
105
|
+
walletStatus: "connected",
|
|
107
106
|
}
|
|
108
107
|
);
|
|
109
108
|
|
|
@@ -111,6 +110,7 @@ describe("processReferral", () => {
|
|
|
111
110
|
type: "arrival",
|
|
112
111
|
referrerClientId: "referrer-client-id",
|
|
113
112
|
referrerMerchantId: "merchant-uuid",
|
|
113
|
+
referrerWallet: undefined,
|
|
114
114
|
referralTimestamp: 1709654400,
|
|
115
115
|
landingUrl: "https://example.com/test",
|
|
116
116
|
});
|
|
@@ -135,6 +135,73 @@ describe("processReferral", () => {
|
|
|
135
135
|
expect(result).toBe("self-referral");
|
|
136
136
|
vi.mocked(utils.getClientId).mockReturnValue("test-client-id");
|
|
137
137
|
});
|
|
138
|
+
|
|
139
|
+
it("should successfully process v2 referral with wallet only (no clientId)", async () => {
|
|
140
|
+
await import("../../utils");
|
|
141
|
+
const { sendInteraction } = await import("../sendInteraction");
|
|
142
|
+
|
|
143
|
+
const referrerWallet =
|
|
144
|
+
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" as Address;
|
|
145
|
+
const v2WithWalletOnly: FrakContextV2 = {
|
|
146
|
+
v: 2,
|
|
147
|
+
m: "merchant-uuid",
|
|
148
|
+
t: 1709654400,
|
|
149
|
+
w: referrerWallet,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const result = await processReferral(mockClient, {
|
|
153
|
+
walletStatus: mockWalletStatus,
|
|
154
|
+
frakContext: v2WithWalletOnly,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(result).toBe("success");
|
|
158
|
+
expect(sendInteraction).toHaveBeenCalledWith(mockClient, {
|
|
159
|
+
type: "arrival",
|
|
160
|
+
referrerClientId: undefined,
|
|
161
|
+
referrerMerchantId: "merchant-uuid",
|
|
162
|
+
referrerWallet,
|
|
163
|
+
referralTimestamp: 1709654400,
|
|
164
|
+
landingUrl: "https://example.com/test",
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should return 'self-referral' when v2 wallet matches current wallet", async () => {
|
|
169
|
+
const v2SelfReferralByWallet: FrakContextV2 = {
|
|
170
|
+
v: 2,
|
|
171
|
+
m: "merchant-uuid",
|
|
172
|
+
t: 1709654400,
|
|
173
|
+
w: mockAddress,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const result = await processReferral(mockClient, {
|
|
177
|
+
walletStatus: mockWalletStatus,
|
|
178
|
+
frakContext: v2SelfReferralByWallet,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(result).toBe("self-referral");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should prefer wallet over clientId for self-referral when both are present", async () => {
|
|
185
|
+
const utils = await import("../../utils");
|
|
186
|
+
// clientId does NOT match current user, but wallet does → still self-referral
|
|
187
|
+
vi.mocked(utils.getClientId).mockReturnValue("some-other-client");
|
|
188
|
+
|
|
189
|
+
const v2Hybrid: FrakContextV2 = {
|
|
190
|
+
v: 2,
|
|
191
|
+
c: "referrer-client-id",
|
|
192
|
+
m: "merchant-uuid",
|
|
193
|
+
t: 1709654400,
|
|
194
|
+
w: mockAddress,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const result = await processReferral(mockClient, {
|
|
198
|
+
walletStatus: mockWalletStatus,
|
|
199
|
+
frakContext: v2Hybrid,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(result).toBe("self-referral");
|
|
203
|
+
vi.mocked(utils.getClientId).mockReturnValue("test-client-id");
|
|
204
|
+
});
|
|
138
205
|
});
|
|
139
206
|
|
|
140
207
|
describe("V1 context (backward compat)", () => {
|
|
@@ -156,10 +223,8 @@ describe("processReferral", () => {
|
|
|
156
223
|
mockClient,
|
|
157
224
|
"user_referred_started",
|
|
158
225
|
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
walletStatus: "connected",
|
|
162
|
-
},
|
|
226
|
+
referrer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
|
|
227
|
+
walletStatus: "connected",
|
|
163
228
|
}
|
|
164
229
|
);
|
|
165
230
|
});
|
|
@@ -54,15 +54,15 @@ function trackArrivalIfValid(
|
|
|
54
54
|
|
|
55
55
|
if (isV2Context(frakContext)) {
|
|
56
56
|
trackEvent(client, "user_referred_started", {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
},
|
|
57
|
+
referrerClientId: frakContext.c,
|
|
58
|
+
referrerWallet: frakContext.w,
|
|
59
|
+
walletStatus: walletStatus?.key,
|
|
61
60
|
});
|
|
62
61
|
sendInteraction(client, {
|
|
63
62
|
type: "arrival",
|
|
64
63
|
referrerClientId: frakContext.c,
|
|
65
64
|
referrerMerchantId: frakContext.m,
|
|
65
|
+
referrerWallet: frakContext.w,
|
|
66
66
|
referralTimestamp: frakContext.t,
|
|
67
67
|
landingUrl,
|
|
68
68
|
});
|
|
@@ -71,10 +71,8 @@ function trackArrivalIfValid(
|
|
|
71
71
|
|
|
72
72
|
if (isV1Context(frakContext)) {
|
|
73
73
|
trackEvent(client, "user_referred_started", {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
walletStatus: walletStatus?.key,
|
|
77
|
-
},
|
|
74
|
+
referrer: frakContext.r,
|
|
75
|
+
walletStatus: walletStatus?.key,
|
|
78
76
|
});
|
|
79
77
|
sendInteraction(client, {
|
|
80
78
|
type: "arrival",
|
|
@@ -111,7 +109,14 @@ function isSelfReferral(
|
|
|
111
109
|
walletStatus?: WalletStatusReturnType
|
|
112
110
|
): boolean {
|
|
113
111
|
if (isV2Context(frakContext)) {
|
|
114
|
-
|
|
112
|
+
// Wallet match takes precedence — it's the strongest signal we have.
|
|
113
|
+
if (frakContext.w && walletStatus?.wallet) {
|
|
114
|
+
return isAddressEqual(frakContext.w, walletStatus.wallet);
|
|
115
|
+
}
|
|
116
|
+
if (frakContext.c) {
|
|
117
|
+
return getClientId() === frakContext.c;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
115
120
|
}
|
|
116
121
|
if (isV1Context(frakContext) && walletStatus?.wallet) {
|
|
117
122
|
return isAddressEqual(frakContext.r, walletStatus.wallet);
|
|
@@ -177,9 +182,7 @@ export function processReferral(
|
|
|
177
182
|
});
|
|
178
183
|
|
|
179
184
|
trackEvent(client, "user_referred_completed", {
|
|
180
|
-
|
|
181
|
-
status: "success",
|
|
182
|
-
},
|
|
185
|
+
status: "success",
|
|
183
186
|
});
|
|
184
187
|
|
|
185
188
|
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
|
+
});
|