@frak-labs/core-sdk 1.0.0-beta.61e6fb99 → 1.0.0-beta.a7645eaa
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cdn/bundle.js +3 -3
- package/dist/actions-3g2MTs6f.cjs +1 -0
- package/dist/actions-C-vPLo1H.js +1 -0
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +2 -2
- package/dist/actions.d.ts +2 -2
- package/dist/actions.js +1 -1
- package/dist/bundle.cjs +1 -1
- package/dist/bundle.d.cts +3 -3
- package/dist/bundle.d.ts +3 -3
- package/dist/bundle.js +1 -1
- package/dist/{index-CGyEOo9J.d.cts → index-BWic1g0J.d.cts} +1 -1
- package/dist/{index-B_Uj-puh.d.ts → index-CNukNJzV.d.ts} +2 -1
- package/dist/{index-Cdf5j2_W.d.ts → index-Du4nB3qO.d.ts} +1 -1
- package/dist/{index-ByVpu25D.d.cts → index-tYNUZNRd.d.cts} +2 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{openSso-B6pD2oA6.d.ts → openSso-3YqtmSkM.d.ts} +18 -7
- package/dist/{openSso-qjaccFd0.d.cts → openSso-SP6T9cHA.d.cts} +18 -7
- package/dist/sdkConfigStore-thuozJuB.cjs +1 -0
- package/dist/sdkConfigStore-ylgC-Tp0.js +1 -0
- package/dist/{src-BxRYON49.js → src-BhrneHv1.js} +1 -1
- package/dist/{src-BqpqVHCq.cjs → src-DkkrSfA4.cjs} +1 -1
- package/package.json +2 -2
- package/src/actions/referral/processReferral.test.ts +125 -0
- package/src/actions/referral/processReferral.ts +22 -6
- package/src/types/context.ts +16 -4
- package/src/types/rpc/interaction.ts +1 -1
- package/src/types/tracking.ts +1 -1
- package/src/utils/FrakContext.test.ts +84 -1
- package/src/utils/FrakContext.ts +23 -10
- package/src/utils/analytics/events/referral.ts +1 -0
- package/dist/actions-Di4welXI.cjs +0 -1
- package/dist/actions-DyMkUe65.js +0 -1
- package/dist/sdkConfigStore-DvwFc6Ym.cjs +0 -1
- package/dist/sdkConfigStore-M37skmM8.js +0 -1
|
@@ -46,7 +46,7 @@ type AttributionParams = {
|
|
|
46
46
|
*/
|
|
47
47
|
type AttributionDefaults = Omit<AttributionParams, "utmContent">;
|
|
48
48
|
type TrackArrivalParams = {
|
|
49
|
-
/**
|
|
49
|
+
/** Sharer wallet address. Accepted in both V1 (legacy) and V2 (authenticated sharer) contexts. */referrerWallet?: Address;
|
|
50
50
|
referrerClientId?: string;
|
|
51
51
|
referrerMerchantId?: string; /** Epoch seconds timestamp from the referral link creation */
|
|
52
52
|
referralTimestamp?: number;
|
|
@@ -951,7 +951,7 @@ type DisplayEmbeddedWalletResultType = {
|
|
|
951
951
|
* @group RPC Schema
|
|
952
952
|
*/
|
|
953
953
|
type SendInteractionParamsType = {
|
|
954
|
-
type: "arrival"; /**
|
|
954
|
+
type: "arrival"; /** Sharer wallet address. Accepted in both V1 (legacy, wallet-only) and V2 (authenticated sharer) contexts. */
|
|
955
955
|
referrerWallet?: Address;
|
|
956
956
|
referrerClientId?: string;
|
|
957
957
|
referrerMerchantId?: string; /** Epoch seconds timestamp from the referral link creation */
|
|
@@ -1296,15 +1296,26 @@ type FrakContextV1 = {
|
|
|
1296
1296
|
/** Referrer wallet address */r: Address;
|
|
1297
1297
|
};
|
|
1298
1298
|
/**
|
|
1299
|
-
* V2 Frak Context — anonymous-first referral context.
|
|
1300
|
-
*
|
|
1299
|
+
* V2 Frak Context — anonymous-first referral context with optional wallet.
|
|
1300
|
+
*
|
|
1301
|
+
* Carries merchant context (`m`) and creation timestamp (`t`) unconditionally.
|
|
1302
|
+
* Identifies the sharer via either the anonymous clientId (`c`) or, when the
|
|
1303
|
+
* sharer is authenticated, the stronger wallet identifier (`w`). A valid V2
|
|
1304
|
+
* context MUST contain at least one of `c` or `w`; both may be present when
|
|
1305
|
+
* a logged-in user shares a link (best attribution signal).
|
|
1306
|
+
*
|
|
1307
|
+
* `w` takes precedence as the source of truth because the wallet is bound to
|
|
1308
|
+
* the user's WebAuthn credential, survives localStorage clears, and is global
|
|
1309
|
+
* across merchants — unlike `c`, which is a per-browser UUID.
|
|
1310
|
+
*
|
|
1301
1311
|
* @ignore
|
|
1302
1312
|
*/
|
|
1303
1313
|
type FrakContextV2 = {
|
|
1304
|
-
/** Version discriminator */v: 2; /**
|
|
1305
|
-
c: string; /** Merchant ID (UUID) */
|
|
1314
|
+
/** Version discriminator */v: 2; /** Merchant ID (UUID) */
|
|
1306
1315
|
m: string; /** Link creation timestamp (epoch seconds) */
|
|
1307
|
-
t: number;
|
|
1316
|
+
t: number; /** Sharer's anonymous clientId (UUID from localStorage). Optional when `w` is provided. */
|
|
1317
|
+
c?: string; /** Sharer's wallet address. Preferred source of truth when the sharer is authenticated. Optional when `c` is provided. */
|
|
1318
|
+
w?: Address;
|
|
1308
1319
|
};
|
|
1309
1320
|
/**
|
|
1310
1321
|
* The current Frak Context — union of all versions.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
let e=require(`viem`),t=require(`@frak-labs/frame-connector`);const n=`frak-client-id`;function r(){return typeof crypto<`u`&&crypto.randomUUID?crypto.randomUUID():`xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e===`x`?t:t&3|8).toString(16)})}function i(){if(typeof window>`u`||!window.localStorage)return console.warn(`[Frak SDK] No Window / localStorage available to save the clientId`),r();let e=localStorage.getItem(n);return e||(e=r(),localStorage.setItem(n,e)),e}function a({domain:t}={}){return(0,e.keccak256)((0,e.toHex)((t??window.location.host).replace(`www.`,``)))}function o(e){return btoa(Array.from(e,e=>String.fromCharCode(e)).join(``)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``)}function s(e){let t=e.length%4;return Uint8Array.from(atob(e.replace(/-/g,`+`).replace(/_/g,`/`).padEnd(e.length+(t===0?0:4-t),`=`)),e=>e.charCodeAt(0))}function c(e){return o((0,t.jsonEncode)(e))}function l(e,t,n,r,i,a){let o=c(u({redirectUrl:t.redirectUrl,directExit:t.directExit,lang:t.lang,merchantId:n,metadata:{name:r,css:a,logoUrl:t.metadata?.logoUrl,homepageLink:t.metadata?.homepageLink},clientId:i})),s=new URL(e);return s.pathname=`/sso`,s.searchParams.set(`p`,o),s.toString()}function u(e){return{r:e.redirectUrl,cId:e.clientId,d:e.directExit,l:e.lang,m:e.merchantId,md:{n:e.metadata?.name,css:e.metadata?.css,l:e.metadata?.logoUrl,h:e.metadata?.homepageLink}}}const d=`menubar=no,status=no,scrollbars=no,fullscreen=no,width=500, height=800`,f=`frak-sso`;async function p(e,t){let{metadata:n,customizations:r,walletUrl:o}=e.config;if(t.openInSameWindow??!!t.redirectUrl)return await e.request({method:`frak_openSso`,params:[t,n.name,r?.css]});let s=t.ssoPopupUrl??l(o??`https://wallet.frak.id`,t,a(),n.name,i(),r?.css),c=window.open(s,f,d);if(!c)throw Error(`Popup was blocked. Please allow popups for this site.`);return c.focus(),await e.request({method:`frak_openSso`,params:[t,n.name,r?.css]})??{}}function ee(e,t,n){if(!e){console.debug(`[Frak] No client provided, skipping event tracking`);return}try{e.openPanel?.track(t,n)}catch(e){console.debug(`[Frak] Failed to track event:`,t,e)}}const m=`https://backend.frak.id`;function te(e){return e.includes(`localhost:3000`)||e.includes(`localhost:3010`)}function h(e){return te(e)?`https://localhost:3030`:e.includes(`wallet-dev.frak.id`)||e.includes(`wallet.gcp-dev.frak.id`)?`https://backend.gcp-dev.frak.id`:m}function g(e){if(e)return h(e);if(typeof window<`u`){let e=window.FrakSetup?.client?.config?.walletUrl;if(e)return h(e)}return m}var _=class extends Map{maxSize;constructor(e){super(),this.maxSize=e}get(e){let t=super.get(e);return super.has(e)&&(super.delete(e),super.set(e,t)),t}set(e,t){if(super.has(e)&&super.delete(e),super.set(e,t),this.maxSize&&this.size>this.maxSize){let e=super.keys().next().value;e!==void 0&&super.delete(e)}return this}};const v=new _(1024),y=new _(1024),b=3e4,x=new _(1024);async function S(e,{cacheKey:t,cacheTime:n=b}){if(n>0){let e=y.get(t);if(e&&Date.now()-e.created<n)return e.data}let r=x.get(t);if(r&&Date.now()-r<1e3)throw Error(`Cache: ${t} recently failed, backing off`);let i=v.get(t);i||(i=e(),v.set(t,i));try{let e=await i;return y.set(t,{data:e,created:Date.now()}),x.delete(t),e}catch(e){throw x.set(t,Date.now()),e}finally{v.delete(t)}}function C(e){return{clear:()=>{v.delete(e),y.delete(e)},has:(t=b)=>{let n=y.get(e);return n?Date.now()-n.created<t:!1}}}function w(){v.clear(),y.clear(),x.clear()}function T(e){return(0,t.jsonDecode)(s(e))}function E(e){return`r`in e&&!(`v`in e)}function D(e){return`v`in e&&e.v===2}const O=`fCtx`;function k(t){if(t)try{if(D(t)){if(!t.m||!t.t)return;let n=t.w&&(0,e.isAddress)(t.w);return!t.c&&!n?void 0:c({v:2,m:t.m,t:t.t,...t.c?{c:t.c}:{},...n?{w:t.w}:{}})}return o((0,e.hexToBytes)(t.r))}catch(e){console.error(`Error compressing Frak context`,{e,context:t})}}function A(t){if(!(!t||t.length===0))try{let n=T(t);if(n&&typeof n==`object`&&n.v===2){if(!n.m||!n.t)return;let t=n.w&&(0,e.isAddress)(n.w);return!n.c&&!t?void 0:{v:2,m:n.m,t:n.t,...n.c?{c:n.c}:{},...t?{w:n.w}:{}}}let r=(0,e.bytesToHex)(s(t),{size:20});if((0,e.isAddress)(r))return{r}}catch(e){console.error(`Error decompressing Frak context`,{e,context:t})}}function ne({url:e}){if(!e)return null;let t=new URL(e).searchParams.get(O);return t?A(t):null}const j=`frak`;function M(e,t){let n=D(e);return{utm_source:t.utmSource??j,utm_medium:t.utmMedium??`referral`,utm_campaign:t.utmCampaign??(n?e.m:void 0),utm_content:t.utmContent,utm_term:t.utmTerm,via:t.via??j,ref:t.ref??(n?e.c:void 0)}}function N(e,t,n){let r=M(t,n??{});for(let[t,n]of Object.entries(r))n===void 0||n===``||e.searchParams.has(t)||e.searchParams.set(t,n)}function P({url:e,context:t,attribution:n}){if(!e)return null;let r=k(t);if(!r)return null;let i=new URL(e);return i.searchParams.set(O,r),N(i,t,n),i.toString()}function F(e){let t=new URL(e);return t.searchParams.delete(O),t.toString()}function I({url:e,context:t}){if(!window.location?.href||typeof window>`u`){console.error(`No window found, can't update context`);return}let n=e??window.location.href,r;r=t===null?F(n):P({url:n,context:t}),r&&window.history.replaceState(null,``,r.toString())}const L={compress:k,decompress:A,parse:ne,update:P,remove:F,replaceUrl:I},R=`__frakSdkConfig`,z=`frak-config-cache`,B=`frak-merchant-id`,V={key:z},H=typeof window<`u`;function U(){return{isResolved:!1,merchantId:``}}let W=null;function G(){if(!H)return null;try{let e=localStorage.getItem(V.key);if(!e)return null;let t=JSON.parse(e);return t.config?.isResolved?(W=t,t):null}catch{return null}}function K(){return(W??G())?.config}function q(){let e=W??G();return e?Date.now()-e.timestamp<3e4:!1}function J(e){if(!(!H||!e.isResolved))try{let t={config:e,timestamp:Date.now()};localStorage.setItem(V.key,JSON.stringify(t)),W=t}catch{}}function Y(){if(H){W=null;try{localStorage.removeItem(V.key)}catch{}}}function X(){H&&(window[R]||(window[R]=K()??U()))}X();function Z(){return H?window[R]??U():U()}function Q(e){H&&window.dispatchEvent(new CustomEvent(`frak:config`,{detail:e}))}function re(e){return e??(H?window.location.hostname:``)}async function ie(e,t,n){try{let r=g(t),i=n?`&lang=${encodeURIComponent(n)}`:``,a=await fetch(`${r}/user/merchant/resolve?domain=${encodeURIComponent(e)}${i}`);if(!a.ok){console.warn(`[Frak SDK] Merchant lookup failed for domain ${e}: ${a.status}`);return}let o=await a.json();if(H)try{sessionStorage.setItem(B,o.merchantId)}catch{}return o}catch(e){console.warn(`[Frak SDK] Failed to fetch merchant config:`,e);return}}const $={getConfig:Z,get isResolved(){return Z().isResolved},get isCacheFresh(){return q()},setCacheScope(e,t){V.key=`${z}:${`${e}:${t??``}`}`,W=null},setConfig(e){if(H&&(window[R]=e),J(e),Q(e),H&&e.merchantId)try{sessionStorage.setItem(B,e.merchantId)}catch{}},reset(){let e=K()??U();H&&(window[R]=e),Q(e)},clearCache(){if(Y(),w(),H)try{sessionStorage.removeItem(B)}catch{}},resolve(e,t,n){let r=re(e);return r?S(async()=>{let e=await ie(r,t,n);if(!e)throw Error(`Config resolution returned empty`);return e},{cacheKey:`sdkConfig:${r}:${n??``}`,cacheTime:1/0}).catch(()=>void 0):Promise.resolve(void 0)},getMerchantId(){let e=Z();if(e.isResolved&&e.merchantId)return e.merchantId;if(H)try{return sessionStorage.getItem(B)??void 0}catch{}},async resolveMerchantId(e,t){return $.getMerchantId()||(await $.resolve(e,t))?.merchantId}};Object.defineProperty(exports,`_`,{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return T}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return S}}),Object.defineProperty(exports,`d`,{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,`f`,{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,`g`,{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,`h`,{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return D}}),Object.defineProperty(exports,`l`,{enumerable:!0,get:function(){return g}}),Object.defineProperty(exports,`m`,{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return L}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return w}}),Object.defineProperty(exports,`p`,{enumerable:!0,get:function(){return f}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return E}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return C}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return $}}),Object.defineProperty(exports,`u`,{enumerable:!0,get:function(){return ee}}),Object.defineProperty(exports,`v`,{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,`y`,{enumerable:!0,get:function(){return i}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{bytesToHex as e,hexToBytes as t,isAddress as n,keccak256 as r,toHex as i}from"viem";import{jsonDecode as a,jsonEncode as o}from"@frak-labs/frame-connector";const s=`frak-client-id`;function c(){return typeof crypto<`u`&&crypto.randomUUID?crypto.randomUUID():`xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e===`x`?t:t&3|8).toString(16)})}function l(){if(typeof window>`u`||!window.localStorage)return console.warn(`[Frak SDK] No Window / localStorage available to save the clientId`),c();let e=localStorage.getItem(s);return e||(e=c(),localStorage.setItem(s,e)),e}function u({domain:e}={}){return r(i((e??window.location.host).replace(`www.`,``)))}function d(e){return btoa(Array.from(e,e=>String.fromCharCode(e)).join(``)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``)}function f(e){let t=e.length%4;return Uint8Array.from(atob(e.replace(/-/g,`+`).replace(/_/g,`/`).padEnd(e.length+(t===0?0:4-t),`=`)),e=>e.charCodeAt(0))}function p(e){return d(o(e))}function m(e,t,n,r,i,a){let o=p(ee({redirectUrl:t.redirectUrl,directExit:t.directExit,lang:t.lang,merchantId:n,metadata:{name:r,css:a,logoUrl:t.metadata?.logoUrl,homepageLink:t.metadata?.homepageLink},clientId:i})),s=new URL(e);return s.pathname=`/sso`,s.searchParams.set(`p`,o),s.toString()}function ee(e){return{r:e.redirectUrl,cId:e.clientId,d:e.directExit,l:e.lang,m:e.merchantId,md:{n:e.metadata?.name,css:e.metadata?.css,l:e.metadata?.logoUrl,h:e.metadata?.homepageLink}}}const h=`menubar=no,status=no,scrollbars=no,fullscreen=no,width=500, height=800`,g=`frak-sso`;async function _(e,t){let{metadata:n,customizations:r,walletUrl:i}=e.config;if(t.openInSameWindow??!!t.redirectUrl)return await e.request({method:`frak_openSso`,params:[t,n.name,r?.css]});let a=t.ssoPopupUrl??m(i??`https://wallet.frak.id`,t,u(),n.name,l(),r?.css),o=window.open(a,g,h);if(!o)throw Error(`Popup was blocked. Please allow popups for this site.`);return o.focus(),await e.request({method:`frak_openSso`,params:[t,n.name,r?.css]})??{}}function v(e,t,n){if(!e){console.debug(`[Frak] No client provided, skipping event tracking`);return}try{e.openPanel?.track(t,n)}catch(e){console.debug(`[Frak] Failed to track event:`,t,e)}}const y=`https://backend.frak.id`;function b(e){return e.includes(`localhost:3000`)||e.includes(`localhost:3010`)}function x(e){return b(e)?`https://localhost:3030`:e.includes(`wallet-dev.frak.id`)||e.includes(`wallet.gcp-dev.frak.id`)?`https://backend.gcp-dev.frak.id`:y}function S(e){if(e)return x(e);if(typeof window<`u`){let e=window.FrakSetup?.client?.config?.walletUrl;if(e)return x(e)}return y}var C=class extends Map{maxSize;constructor(e){super(),this.maxSize=e}get(e){let t=super.get(e);return super.has(e)&&(super.delete(e),super.set(e,t)),t}set(e,t){if(super.has(e)&&super.delete(e),super.set(e,t),this.maxSize&&this.size>this.maxSize){let e=super.keys().next().value;e!==void 0&&super.delete(e)}return this}};const w=new C(1024),T=new C(1024),E=3e4,D=new C(1024);async function O(e,{cacheKey:t,cacheTime:n=E}){if(n>0){let e=T.get(t);if(e&&Date.now()-e.created<n)return e.data}let r=D.get(t);if(r&&Date.now()-r<1e3)throw Error(`Cache: ${t} recently failed, backing off`);let i=w.get(t);i||(i=e(),w.set(t,i));try{let e=await i;return T.set(t,{data:e,created:Date.now()}),D.delete(t),e}catch(e){throw D.set(t,Date.now()),e}finally{w.delete(t)}}function k(e){return{clear:()=>{w.delete(e),T.delete(e)},has:(t=E)=>{let n=T.get(e);return n?Date.now()-n.created<t:!1}}}function A(){w.clear(),T.clear(),D.clear()}function j(e){return a(f(e))}function M(e){return`r`in e&&!(`v`in e)}function N(e){return`v`in e&&e.v===2}const P=`fCtx`;function F(e){if(e)try{if(N(e)){if(!e.m||!e.t)return;let t=e.w&&n(e.w);return!e.c&&!t?void 0:p({v:2,m:e.m,t:e.t,...e.c?{c:e.c}:{},...t?{w:e.w}:{}})}return d(t(e.r))}catch(t){console.error(`Error compressing Frak context`,{e:t,context:e})}}function I(t){if(!(!t||t.length===0))try{let r=j(t);if(r&&typeof r==`object`&&r.v===2){if(!r.m||!r.t)return;let e=r.w&&n(r.w);return!r.c&&!e?void 0:{v:2,m:r.m,t:r.t,...r.c?{c:r.c}:{},...e?{w:r.w}:{}}}let i=e(f(t),{size:20});if(n(i))return{r:i}}catch(e){console.error(`Error decompressing Frak context`,{e,context:t})}}function te({url:e}){if(!e)return null;let t=new URL(e).searchParams.get(P);return t?I(t):null}const L=`frak`;function R(e,t){let n=N(e);return{utm_source:t.utmSource??L,utm_medium:t.utmMedium??`referral`,utm_campaign:t.utmCampaign??(n?e.m:void 0),utm_content:t.utmContent,utm_term:t.utmTerm,via:t.via??L,ref:t.ref??(n?e.c:void 0)}}function z(e,t,n){let r=R(t,n??{});for(let[t,n]of Object.entries(r))n===void 0||n===``||e.searchParams.has(t)||e.searchParams.set(t,n)}function B({url:e,context:t,attribution:n}){if(!e)return null;let r=F(t);if(!r)return null;let i=new URL(e);return i.searchParams.set(P,r),z(i,t,n),i.toString()}function V(e){let t=new URL(e);return t.searchParams.delete(P),t.toString()}function ne({url:e,context:t}){if(!window.location?.href||typeof window>`u`){console.error(`No window found, can't update context`);return}let n=e??window.location.href,r;r=t===null?V(n):B({url:n,context:t}),r&&window.history.replaceState(null,``,r.toString())}const re={compress:F,decompress:I,parse:te,update:B,remove:V,replaceUrl:ne},H=`__frakSdkConfig`,U=`frak-config-cache`,W=`frak-merchant-id`,G={key:U},K=typeof window<`u`;function q(){return{isResolved:!1,merchantId:``}}let J=null;function Y(){if(!K)return null;try{let e=localStorage.getItem(G.key);if(!e)return null;let t=JSON.parse(e);return t.config?.isResolved?(J=t,t):null}catch{return null}}function X(){return(J??Y())?.config}function ie(){let e=J??Y();return e?Date.now()-e.timestamp<3e4:!1}function ae(e){if(!(!K||!e.isResolved))try{let t={config:e,timestamp:Date.now()};localStorage.setItem(G.key,JSON.stringify(t)),J=t}catch{}}function oe(){if(K){J=null;try{localStorage.removeItem(G.key)}catch{}}}function se(){K&&(window[H]||(window[H]=X()??q()))}se();function Z(){return K?window[H]??q():q()}function Q(e){K&&window.dispatchEvent(new CustomEvent(`frak:config`,{detail:e}))}function ce(e){return e??(K?window.location.hostname:``)}async function le(e,t,n){try{let r=S(t),i=n?`&lang=${encodeURIComponent(n)}`:``,a=await fetch(`${r}/user/merchant/resolve?domain=${encodeURIComponent(e)}${i}`);if(!a.ok){console.warn(`[Frak SDK] Merchant lookup failed for domain ${e}: ${a.status}`);return}let o=await a.json();if(K)try{sessionStorage.setItem(W,o.merchantId)}catch{}return o}catch(e){console.warn(`[Frak SDK] Failed to fetch merchant config:`,e);return}}const $={getConfig:Z,get isResolved(){return Z().isResolved},get isCacheFresh(){return ie()},setCacheScope(e,t){G.key=`${U}:${`${e}:${t??``}`}`,J=null},setConfig(e){if(K&&(window[H]=e),ae(e),Q(e),K&&e.merchantId)try{sessionStorage.setItem(W,e.merchantId)}catch{}},reset(){let e=X()??q();K&&(window[H]=e),Q(e)},clearCache(){if(oe(),A(),K)try{sessionStorage.removeItem(W)}catch{}},resolve(e,t,n){let r=ce(e);return r?O(async()=>{let e=await le(r,t,n);if(!e)throw Error(`Config resolution returned empty`);return e},{cacheKey:`sdkConfig:${r}:${n??``}`,cacheTime:1/0}).catch(()=>void 0):Promise.resolve(void 0)},getMerchantId(){let e=Z();if(e.isResolved&&e.merchantId)return e.merchantId;if(K)try{return sessionStorage.getItem(W)??void 0}catch{}},async resolveMerchantId(e,t){return $.getMerchantId()||(await $.resolve(e,t))?.merchantId}};export{d as _,j as a,O as c,_ as d,h as f,f as g,p as h,N as i,S as l,m,re as n,A as o,g as p,M as r,k as s,$ as t,v as u,u as v,l as y};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{o as e,t,y as n}from"./sdkConfigStore-
|
|
1
|
+
import{o as e,t,y as n}from"./sdkConfigStore-ylgC-Tp0.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
2
|
Debug Information:
|
|
3
3
|
-----------------
|
|
4
4
|
Timestamp: ${t.timestamp}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const e=require(`./sdkConfigStore-
|
|
1
|
+
const e=require(`./sdkConfigStore-thuozJuB.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
2
|
Debug Information:
|
|
3
3
|
-----------------
|
|
4
4
|
Timestamp: ${t.timestamp}
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"url": "https://twitter.com/QNivelais"
|
|
12
12
|
}
|
|
13
13
|
],
|
|
14
|
-
"version": "1.0.0-beta.
|
|
14
|
+
"version": "1.0.0-beta.a7645eaa",
|
|
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,7 +91,7 @@
|
|
|
91
91
|
"viem": "^2.x"
|
|
92
92
|
},
|
|
93
93
|
"dependencies": {
|
|
94
|
-
"@frak-labs/frame-connector": "0.2.0-beta.
|
|
94
|
+
"@frak-labs/frame-connector": "0.2.0-beta.a7645eaa",
|
|
95
95
|
"@openpanel/web": "^1.2.0"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
@@ -101,6 +101,7 @@ describe("processReferral", () => {
|
|
|
101
101
|
"user_referred_started",
|
|
102
102
|
{
|
|
103
103
|
referrerClientId: "referrer-client-id",
|
|
104
|
+
referrerWallet: undefined,
|
|
104
105
|
walletStatus: "connected",
|
|
105
106
|
}
|
|
106
107
|
);
|
|
@@ -109,6 +110,7 @@ describe("processReferral", () => {
|
|
|
109
110
|
type: "arrival",
|
|
110
111
|
referrerClientId: "referrer-client-id",
|
|
111
112
|
referrerMerchantId: "merchant-uuid",
|
|
113
|
+
referrerWallet: undefined,
|
|
112
114
|
referralTimestamp: 1709654400,
|
|
113
115
|
landingUrl: "https://example.com/test",
|
|
114
116
|
});
|
|
@@ -133,6 +135,73 @@ describe("processReferral", () => {
|
|
|
133
135
|
expect(result).toBe("self-referral");
|
|
134
136
|
vi.mocked(utils.getClientId).mockReturnValue("test-client-id");
|
|
135
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
|
+
});
|
|
136
205
|
});
|
|
137
206
|
|
|
138
207
|
describe("V1 context (backward compat)", () => {
|
|
@@ -198,6 +267,7 @@ describe("processReferral", () => {
|
|
|
198
267
|
v: 2,
|
|
199
268
|
c: "test-client-id",
|
|
200
269
|
m: "merchant-uuid",
|
|
270
|
+
w: mockAddress,
|
|
201
271
|
}),
|
|
202
272
|
});
|
|
203
273
|
});
|
|
@@ -225,4 +295,59 @@ describe("processReferral", () => {
|
|
|
225
295
|
context: null,
|
|
226
296
|
});
|
|
227
297
|
});
|
|
298
|
+
|
|
299
|
+
it("should emit wallet in replacement context when alwaysAppendUrl is true and user is connected", async () => {
|
|
300
|
+
const utils = await import("../../utils");
|
|
301
|
+
vi.mocked(utils.getClientId).mockReturnValue(null as never);
|
|
302
|
+
|
|
303
|
+
const v2Context: FrakContextV2 = {
|
|
304
|
+
v: 2,
|
|
305
|
+
c: "referrer-client-id",
|
|
306
|
+
m: "merchant-uuid",
|
|
307
|
+
t: 1709654400,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
await processReferral(mockClient, {
|
|
311
|
+
walletStatus: mockWalletStatus,
|
|
312
|
+
frakContext: v2Context,
|
|
313
|
+
options: { alwaysAppendUrl: true },
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// clientId is null, but wallet is available — should still emit {w, m}
|
|
317
|
+
expect(utils.FrakContextManager.replaceUrl).toHaveBeenCalledWith({
|
|
318
|
+
url: window.location.href,
|
|
319
|
+
context: expect.objectContaining({
|
|
320
|
+
v: 2,
|
|
321
|
+
m: "merchant-uuid",
|
|
322
|
+
w: mockAddress,
|
|
323
|
+
}),
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
vi.mocked(utils.getClientId).mockReturnValue("test-client-id");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("should return null replacement context when both clientId and wallet are missing", async () => {
|
|
330
|
+
const utils = await import("../../utils");
|
|
331
|
+
vi.mocked(utils.getClientId).mockReturnValue(null as never);
|
|
332
|
+
|
|
333
|
+
const v2Context: FrakContextV2 = {
|
|
334
|
+
v: 2,
|
|
335
|
+
c: "referrer-client-id",
|
|
336
|
+
m: "merchant-uuid",
|
|
337
|
+
t: 1709654400,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
await processReferral(mockClient, {
|
|
341
|
+
walletStatus: { key: "not-connected" as const },
|
|
342
|
+
frakContext: v2Context,
|
|
343
|
+
options: { alwaysAppendUrl: true },
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(utils.FrakContextManager.replaceUrl).toHaveBeenCalledWith({
|
|
347
|
+
url: window.location.href,
|
|
348
|
+
context: null,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
vi.mocked(utils.getClientId).mockReturnValue("test-client-id");
|
|
352
|
+
});
|
|
228
353
|
});
|
|
@@ -55,12 +55,14 @@ function trackArrivalIfValid(
|
|
|
55
55
|
if (isV2Context(frakContext)) {
|
|
56
56
|
trackEvent(client, "user_referred_started", {
|
|
57
57
|
referrerClientId: frakContext.c,
|
|
58
|
+
referrerWallet: frakContext.w,
|
|
58
59
|
walletStatus: walletStatus?.key,
|
|
59
60
|
});
|
|
60
61
|
sendInteraction(client, {
|
|
61
62
|
type: "arrival",
|
|
62
63
|
referrerClientId: frakContext.c,
|
|
63
64
|
referrerMerchantId: frakContext.m,
|
|
65
|
+
referrerWallet: frakContext.w,
|
|
64
66
|
referralTimestamp: frakContext.t,
|
|
65
67
|
landingUrl,
|
|
66
68
|
});
|
|
@@ -85,16 +87,23 @@ function trackArrivalIfValid(
|
|
|
85
87
|
|
|
86
88
|
/**
|
|
87
89
|
* Build a V2 context representing the current user for URL replacement.
|
|
88
|
-
*
|
|
90
|
+
*
|
|
91
|
+
* Emits both `c` (anonymous clientId) and `w` (wallet) when available — wallet
|
|
92
|
+
* is the preferred identity signal and takes attribution precedence downstream.
|
|
93
|
+
* Returns null when neither identifier is available.
|
|
89
94
|
*/
|
|
90
|
-
function buildCurrentUserContext(
|
|
95
|
+
function buildCurrentUserContext(
|
|
96
|
+
merchantId: string,
|
|
97
|
+
wallet?: WalletStatusReturnType["wallet"]
|
|
98
|
+
): FrakContextV2 | null {
|
|
91
99
|
const clientId = getClientId();
|
|
92
|
-
if (!clientId) return null;
|
|
100
|
+
if (!clientId && !wallet) return null;
|
|
93
101
|
return {
|
|
94
102
|
v: 2,
|
|
95
|
-
c: clientId,
|
|
96
103
|
m: merchantId,
|
|
97
104
|
t: Math.floor(Date.now() / 1000),
|
|
105
|
+
...(clientId ? { c: clientId } : {}),
|
|
106
|
+
...(wallet ? { w: wallet } : {}),
|
|
98
107
|
};
|
|
99
108
|
}
|
|
100
109
|
|
|
@@ -107,7 +116,14 @@ function isSelfReferral(
|
|
|
107
116
|
walletStatus?: WalletStatusReturnType
|
|
108
117
|
): boolean {
|
|
109
118
|
if (isV2Context(frakContext)) {
|
|
110
|
-
|
|
119
|
+
// Wallet match takes precedence — it's the strongest signal we have.
|
|
120
|
+
if (frakContext.w && walletStatus?.wallet) {
|
|
121
|
+
return isAddressEqual(frakContext.w, walletStatus.wallet);
|
|
122
|
+
}
|
|
123
|
+
if (frakContext.c) {
|
|
124
|
+
return getClientId() === frakContext.c;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
111
127
|
}
|
|
112
128
|
if (isV1Context(frakContext) && walletStatus?.wallet) {
|
|
113
129
|
return isAddressEqual(frakContext.r, walletStatus.wallet);
|
|
@@ -164,7 +180,7 @@ export function processReferral(
|
|
|
164
180
|
|
|
165
181
|
const replaceContext =
|
|
166
182
|
options?.alwaysAppendUrl && contextMerchantId
|
|
167
|
-
? buildCurrentUserContext(contextMerchantId)
|
|
183
|
+
? buildCurrentUserContext(contextMerchantId, walletStatus?.wallet)
|
|
168
184
|
: null;
|
|
169
185
|
|
|
170
186
|
FrakContextManager.replaceUrl({
|
package/src/types/context.ts
CHANGED
|
@@ -11,19 +11,31 @@ export type FrakContextV1 = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* V2 Frak Context — anonymous-first referral context.
|
|
15
|
-
*
|
|
14
|
+
* V2 Frak Context — anonymous-first referral context with optional wallet.
|
|
15
|
+
*
|
|
16
|
+
* Carries merchant context (`m`) and creation timestamp (`t`) unconditionally.
|
|
17
|
+
* Identifies the sharer via either the anonymous clientId (`c`) or, when the
|
|
18
|
+
* sharer is authenticated, the stronger wallet identifier (`w`). A valid V2
|
|
19
|
+
* context MUST contain at least one of `c` or `w`; both may be present when
|
|
20
|
+
* a logged-in user shares a link (best attribution signal).
|
|
21
|
+
*
|
|
22
|
+
* `w` takes precedence as the source of truth because the wallet is bound to
|
|
23
|
+
* the user's WebAuthn credential, survives localStorage clears, and is global
|
|
24
|
+
* across merchants — unlike `c`, which is a per-browser UUID.
|
|
25
|
+
*
|
|
16
26
|
* @ignore
|
|
17
27
|
*/
|
|
18
28
|
export type FrakContextV2 = {
|
|
19
29
|
/** Version discriminator */
|
|
20
30
|
v: 2;
|
|
21
|
-
/** Sharer's anonymous clientId (UUID from localStorage) */
|
|
22
|
-
c: string;
|
|
23
31
|
/** Merchant ID (UUID) */
|
|
24
32
|
m: string;
|
|
25
33
|
/** Link creation timestamp (epoch seconds) */
|
|
26
34
|
t: number;
|
|
35
|
+
/** Sharer's anonymous clientId (UUID from localStorage). Optional when `w` is provided. */
|
|
36
|
+
c?: string;
|
|
37
|
+
/** Sharer's wallet address. Preferred source of truth when the sharer is authenticated. Optional when `c` is provided. */
|
|
38
|
+
w?: Address;
|
|
27
39
|
};
|
|
28
40
|
|
|
29
41
|
/**
|
|
@@ -11,7 +11,7 @@ import type { Address } from "viem";
|
|
|
11
11
|
export type SendInteractionParamsType =
|
|
12
12
|
| {
|
|
13
13
|
type: "arrival";
|
|
14
|
-
/**
|
|
14
|
+
/** Sharer wallet address. Accepted in both V1 (legacy, wallet-only) and V2 (authenticated sharer) contexts. */
|
|
15
15
|
referrerWallet?: Address;
|
|
16
16
|
referrerClientId?: string;
|
|
17
17
|
referrerMerchantId?: string;
|
package/src/types/tracking.ts
CHANGED
|
@@ -45,7 +45,7 @@ export type AttributionParams = {
|
|
|
45
45
|
export type AttributionDefaults = Omit<AttributionParams, "utmContent">;
|
|
46
46
|
|
|
47
47
|
export type TrackArrivalParams = {
|
|
48
|
-
/**
|
|
48
|
+
/** Sharer wallet address. Accepted in both V1 (legacy) and V2 (authenticated sharer) contexts. */
|
|
49
49
|
referrerWallet?: Address;
|
|
50
50
|
referrerClientId?: string;
|
|
51
51
|
referrerMerchantId?: string;
|
|
@@ -42,7 +42,7 @@ describe("FrakContextManager", () => {
|
|
|
42
42
|
expect(result).not.toMatch(/[+/=]/);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
it("should return undefined when v2 context
|
|
45
|
+
it("should return undefined when v2 context has neither clientId nor wallet", () => {
|
|
46
46
|
const partial = { v: 2 as const, m: "m", t: 123 };
|
|
47
47
|
const result = FrakContextManager.compress(
|
|
48
48
|
partial as FrakContextV2
|
|
@@ -50,6 +50,33 @@ describe("FrakContextManager", () => {
|
|
|
50
50
|
expect(result).toBeUndefined();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
it("should compress v2 context with wallet only (no clientId)", () => {
|
|
54
|
+
const v2WithWalletOnly: FrakContextV2 = {
|
|
55
|
+
v: 2,
|
|
56
|
+
m: "merchant-uuid-1234",
|
|
57
|
+
t: 1709654400,
|
|
58
|
+
w: "0x1234567890123456789012345678901234567890" as Address,
|
|
59
|
+
};
|
|
60
|
+
const result = FrakContextManager.compress(v2WithWalletOnly);
|
|
61
|
+
expect(result).toBeDefined();
|
|
62
|
+
const decompressed = FrakContextManager.decompress(result);
|
|
63
|
+
expect(decompressed).toEqual(v2WithWalletOnly);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should compress v2 context with both clientId and wallet", () => {
|
|
67
|
+
const v2Hybrid: FrakContextV2 = {
|
|
68
|
+
v: 2,
|
|
69
|
+
c: "test-client-id-uuid",
|
|
70
|
+
m: "merchant-uuid-1234",
|
|
71
|
+
t: 1709654400,
|
|
72
|
+
w: "0x1234567890123456789012345678901234567890" as Address,
|
|
73
|
+
};
|
|
74
|
+
const result = FrakContextManager.compress(v2Hybrid);
|
|
75
|
+
expect(result).toBeDefined();
|
|
76
|
+
const decompressed = FrakContextManager.decompress(result);
|
|
77
|
+
expect(decompressed).toEqual(v2Hybrid);
|
|
78
|
+
});
|
|
79
|
+
|
|
53
80
|
it("should return undefined when v2 context is missing merchantId", () => {
|
|
54
81
|
const partial = { v: 2 as const, c: "c", t: 123 };
|
|
55
82
|
const result = FrakContextManager.compress(
|
|
@@ -65,6 +92,40 @@ describe("FrakContextManager", () => {
|
|
|
65
92
|
);
|
|
66
93
|
expect(result).toBeUndefined();
|
|
67
94
|
});
|
|
95
|
+
|
|
96
|
+
it("should reject v2 context with a malformed wallet address", () => {
|
|
97
|
+
const partial = {
|
|
98
|
+
v: 2 as const,
|
|
99
|
+
m: "merchant-uuid",
|
|
100
|
+
t: 1709654400,
|
|
101
|
+
w: "0xnot-a-valid-address" as Address,
|
|
102
|
+
};
|
|
103
|
+
const result = FrakContextManager.compress(
|
|
104
|
+
partial as FrakContextV2
|
|
105
|
+
);
|
|
106
|
+
// Invalid wallet → falls back to clientId requirement; absent here → undefined
|
|
107
|
+
expect(result).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should drop a malformed wallet but keep a valid clientId", () => {
|
|
111
|
+
const hybrid = {
|
|
112
|
+
v: 2 as const,
|
|
113
|
+
c: "valid-client-id",
|
|
114
|
+
m: "merchant-uuid",
|
|
115
|
+
t: 1709654400,
|
|
116
|
+
w: "0xnot-a-valid-address" as Address,
|
|
117
|
+
};
|
|
118
|
+
const compressed = FrakContextManager.compress(
|
|
119
|
+
hybrid as FrakContextV2
|
|
120
|
+
);
|
|
121
|
+
const decompressed = FrakContextManager.decompress(compressed);
|
|
122
|
+
expect(decompressed).toEqual({
|
|
123
|
+
v: 2,
|
|
124
|
+
c: "valid-client-id",
|
|
125
|
+
m: "merchant-uuid",
|
|
126
|
+
t: 1709654400,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
68
129
|
});
|
|
69
130
|
|
|
70
131
|
describe("decompress", () => {
|
|
@@ -74,6 +135,28 @@ describe("FrakContextManager", () => {
|
|
|
74
135
|
|
|
75
136
|
expect(decompressed).toEqual(v2Context);
|
|
76
137
|
});
|
|
138
|
+
|
|
139
|
+
it("should drop malformed `w` field on decompress (defense against crafted URLs)", async () => {
|
|
140
|
+
// Manually compress a payload bypassing client-side validation
|
|
141
|
+
// by reaching into the underlying compressJsonToB64.
|
|
142
|
+
const { compressJsonToB64 } = await import(
|
|
143
|
+
"./compression/compress"
|
|
144
|
+
);
|
|
145
|
+
const malformed = compressJsonToB64({
|
|
146
|
+
v: 2,
|
|
147
|
+
c: "valid-client-id",
|
|
148
|
+
m: "merchant-uuid",
|
|
149
|
+
t: 1709654400,
|
|
150
|
+
w: "0xnot-a-real-address",
|
|
151
|
+
});
|
|
152
|
+
const result = FrakContextManager.decompress(malformed);
|
|
153
|
+
expect(result).toEqual({
|
|
154
|
+
v: 2,
|
|
155
|
+
c: "valid-client-id",
|
|
156
|
+
m: "merchant-uuid",
|
|
157
|
+
t: 1709654400,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
77
160
|
});
|
|
78
161
|
|
|
79
162
|
describe("parse", () => {
|
package/src/utils/FrakContext.ts
CHANGED
|
@@ -28,13 +28,17 @@ function compress(context?: FrakContextV1 | FrakContextV2): string | undefined {
|
|
|
28
28
|
if (!context) return;
|
|
29
29
|
try {
|
|
30
30
|
if (isV2Context(context)) {
|
|
31
|
-
// Runtime validation:
|
|
32
|
-
|
|
31
|
+
// Runtime validation: m + t are always required, and at least one of
|
|
32
|
+
// c (anonymous fingerprint) or w (valid wallet) must be present.
|
|
33
|
+
if (!context.m || !context.t) return undefined;
|
|
34
|
+
const hasValidWallet = context.w && isAddress(context.w);
|
|
35
|
+
if (!context.c && !hasValidWallet) return undefined;
|
|
33
36
|
return compressJsonToB64({
|
|
34
37
|
v: 2,
|
|
35
|
-
c: context.c,
|
|
36
38
|
m: context.m,
|
|
37
39
|
t: context.t,
|
|
40
|
+
...(context.c ? { c: context.c } : {}),
|
|
41
|
+
...(hasValidWallet ? { w: context.w } : {}),
|
|
38
42
|
});
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -61,10 +65,18 @@ function decompress(context?: string): FrakContext | undefined {
|
|
|
61
65
|
// Try V2 JSON first — V2 payloads are longer than V1's 20-byte address
|
|
62
66
|
const json = decompressJsonFromB64<FrakContextV2>(context);
|
|
63
67
|
if (json && typeof json === "object" && json.v === 2) {
|
|
64
|
-
if (json.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
if (!json.m || !json.t) return undefined;
|
|
69
|
+
// Validate `w` with isAddress() — protects self-referral checks and
|
|
70
|
+
// URL replacement from crafted payloads carrying a malformed wallet.
|
|
71
|
+
const hasValidWallet = json.w && isAddress(json.w);
|
|
72
|
+
if (!json.c && !hasValidWallet) return undefined;
|
|
73
|
+
return {
|
|
74
|
+
v: 2,
|
|
75
|
+
m: json.m,
|
|
76
|
+
t: json.t,
|
|
77
|
+
...(json.c ? { c: json.c } : {}),
|
|
78
|
+
...(hasValidWallet ? { w: json.w } : {}),
|
|
79
|
+
};
|
|
68
80
|
}
|
|
69
81
|
|
|
70
82
|
// Fall back to V1: raw 20-byte address
|
|
@@ -109,9 +121,10 @@ const DEFAULT_ATTRIBUTION_SOURCE = "frak";
|
|
|
109
121
|
/**
|
|
110
122
|
* Resolve attribution defaults from the provided context.
|
|
111
123
|
*
|
|
112
|
-
* V2 contexts expose the merchantId (`m`) and
|
|
113
|
-
* `utm_campaign` and `ref` respectively.
|
|
114
|
-
*
|
|
124
|
+
* V2 contexts expose the merchantId (`m`) and, when anonymous, the clientId
|
|
125
|
+
* (`c`), which feed `utm_campaign` and `ref` respectively. When V2 only carries
|
|
126
|
+
* a wallet (`w`), `ref` is intentionally left unset — we don't want wallet
|
|
127
|
+
* addresses leaking into UTM params. V1 contexts have no equivalent.
|
|
115
128
|
*/
|
|
116
129
|
function resolveAttributionValues(
|
|
117
130
|
context: FrakContextV1 | FrakContextV2,
|