@frak-labs/core-sdk 0.1.0-beta.6e0d8026 → 0.1.0-beta.8d103039
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/package.json +3 -2
- package/src/actions/displayEmbeddedWallet.ts +20 -0
- package/src/actions/displayModal.ts +131 -0
- package/src/actions/getProductInformation.ts +14 -0
- package/src/actions/index.ts +29 -0
- package/src/actions/openSso.ts +116 -0
- package/src/actions/prepareSso.ts +48 -0
- package/src/actions/referral/processReferral.ts +230 -0
- package/src/actions/referral/referralInteraction.ts +57 -0
- package/src/actions/sendInteraction.ts +32 -0
- package/src/actions/trackPurchaseStatus.ts +53 -0
- package/src/actions/watchWalletStatus.ts +94 -0
- package/src/actions/wrapper/modalBuilder.ts +212 -0
- package/src/actions/wrapper/sendTransaction.ts +62 -0
- package/src/actions/wrapper/siweAuthenticate.ts +94 -0
- package/src/bundle.ts +3 -0
- package/src/clients/DebugInfo.ts +182 -0
- package/src/clients/createIFrameFrakClient.ts +287 -0
- package/src/clients/index.ts +3 -0
- package/src/clients/setupClient.ts +71 -0
- package/src/clients/transports/iframeLifecycleManager.ts +88 -0
- package/src/constants/interactionTypes.ts +44 -0
- package/src/constants/locales.ts +14 -0
- package/src/constants/productTypes.ts +33 -0
- package/src/index.ts +103 -0
- package/src/interactions/index.ts +5 -0
- package/src/interactions/pressEncoder.ts +53 -0
- package/src/interactions/purchaseEncoder.ts +94 -0
- package/src/interactions/referralEncoder.ts +47 -0
- package/src/interactions/retailEncoder.ts +37 -0
- package/src/interactions/webshopEncoder.ts +30 -0
- package/src/types/client.ts +14 -0
- package/src/types/compression.ts +22 -0
- package/src/types/config.ts +111 -0
- package/src/types/context.ts +13 -0
- package/src/types/index.ts +70 -0
- package/src/types/lifecycle/client.ts +46 -0
- package/src/types/lifecycle/iframe.ts +35 -0
- package/src/types/lifecycle/index.ts +2 -0
- package/src/types/rpc/displayModal.ts +84 -0
- package/src/types/rpc/embedded/index.ts +68 -0
- package/src/types/rpc/embedded/loggedIn.ts +55 -0
- package/src/types/rpc/embedded/loggedOut.ts +28 -0
- package/src/types/rpc/interaction.ts +43 -0
- package/src/types/rpc/modal/final.ts +46 -0
- package/src/types/rpc/modal/generic.ts +46 -0
- package/src/types/rpc/modal/index.ts +20 -0
- package/src/types/rpc/modal/login.ts +32 -0
- package/src/types/rpc/modal/openSession.ts +25 -0
- package/src/types/rpc/modal/siweAuthenticate.ts +37 -0
- package/src/types/rpc/modal/transaction.ts +33 -0
- package/src/types/rpc/productInformation.ts +59 -0
- package/src/types/rpc/sso.ts +80 -0
- package/src/types/rpc/walletStatus.ts +35 -0
- package/src/types/rpc.ts +158 -0
- package/src/types/transport.ts +34 -0
- package/src/utils/FrakContext.ts +152 -0
- package/src/utils/compression/b64.ts +29 -0
- package/src/utils/compression/compress.ts +11 -0
- package/src/utils/compression/decompress.ts +11 -0
- package/src/utils/compression/index.ts +3 -0
- package/src/utils/computeProductId.ts +11 -0
- package/src/utils/constants.ts +4 -0
- package/src/utils/formatAmount.ts +18 -0
- package/src/utils/getCurrencyAmountKey.ts +15 -0
- package/src/utils/getSupportedCurrency.ts +14 -0
- package/src/utils/getSupportedLocale.ts +16 -0
- package/src/utils/iframeHelper.ts +142 -0
- package/src/utils/index.ts +21 -0
- package/src/utils/sso.ts +119 -0
- package/src/utils/ssoUrlListener.ts +60 -0
- package/src/utils/trackEvent.ts +26 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { FrakWalletSdkConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base props for the iframe
|
|
5
|
+
* @ignore
|
|
6
|
+
*/
|
|
7
|
+
export const baseIframeProps = {
|
|
8
|
+
id: "frak-wallet",
|
|
9
|
+
name: "frak-wallet",
|
|
10
|
+
title: "Frak Wallet",
|
|
11
|
+
allow: "publickey-credentials-get *; clipboard-write; web-share *",
|
|
12
|
+
style: {
|
|
13
|
+
width: "0",
|
|
14
|
+
height: "0",
|
|
15
|
+
border: "0",
|
|
16
|
+
position: "absolute",
|
|
17
|
+
zIndex: 2000001,
|
|
18
|
+
top: "-1000px",
|
|
19
|
+
left: "-1000px",
|
|
20
|
+
colorScheme: "auto",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create the Frak iframe
|
|
26
|
+
* @param args
|
|
27
|
+
* @param args.walletBaseUrl - Use `config.walletUrl` instead. Will be removed in future versions.
|
|
28
|
+
* @param args.config - The configuration object containing iframe options, including the replacement for `walletBaseUrl`.
|
|
29
|
+
*/
|
|
30
|
+
export function createIframe({
|
|
31
|
+
walletBaseUrl,
|
|
32
|
+
config,
|
|
33
|
+
}: { walletBaseUrl?: string; config?: FrakWalletSdkConfig }): Promise<
|
|
34
|
+
HTMLIFrameElement | undefined
|
|
35
|
+
> {
|
|
36
|
+
// Check if the iframe is already created
|
|
37
|
+
const alreadyCreatedIFrame = document.querySelector("#frak-wallet");
|
|
38
|
+
|
|
39
|
+
// If the iframe is already created, remove it
|
|
40
|
+
if (alreadyCreatedIFrame) {
|
|
41
|
+
alreadyCreatedIFrame.remove();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const iframe = document.createElement("iframe");
|
|
45
|
+
|
|
46
|
+
// Set the base iframe props
|
|
47
|
+
iframe.id = baseIframeProps.id;
|
|
48
|
+
iframe.name = baseIframeProps.name;
|
|
49
|
+
iframe.allow = baseIframeProps.allow;
|
|
50
|
+
iframe.style.zIndex = baseIframeProps.style.zIndex.toString();
|
|
51
|
+
|
|
52
|
+
changeIframeVisibility({ iframe, isVisible: false });
|
|
53
|
+
document.body.appendChild(iframe);
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
iframe?.addEventListener("load", () => resolve(iframe));
|
|
57
|
+
iframe.src = `${config?.walletUrl ?? walletBaseUrl ?? "https://wallet.frak.id"}/listener`;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Change the visibility of the given iframe
|
|
62
|
+
* @ignore
|
|
63
|
+
*/
|
|
64
|
+
export function changeIframeVisibility({
|
|
65
|
+
iframe,
|
|
66
|
+
isVisible,
|
|
67
|
+
}: {
|
|
68
|
+
iframe: HTMLIFrameElement;
|
|
69
|
+
isVisible: boolean;
|
|
70
|
+
}) {
|
|
71
|
+
if (!isVisible) {
|
|
72
|
+
iframe.style.width = "0";
|
|
73
|
+
iframe.style.height = "0";
|
|
74
|
+
iframe.style.border = "0";
|
|
75
|
+
iframe.style.position = "fixed";
|
|
76
|
+
iframe.style.top = "-1000px";
|
|
77
|
+
iframe.style.left = "-1000px";
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
iframe.style.position = "fixed";
|
|
82
|
+
iframe.style.top = "0";
|
|
83
|
+
iframe.style.left = "0";
|
|
84
|
+
iframe.style.width = "100%";
|
|
85
|
+
iframe.style.height = "100%";
|
|
86
|
+
iframe.style.pointerEvents = "auto";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Find an iframe within window.opener by pathname
|
|
91
|
+
*
|
|
92
|
+
* When a popup is opened via window.open from an iframe, window.opener points to
|
|
93
|
+
* the parent window, not the iframe itself. This utility searches through all frames
|
|
94
|
+
* in window.opener to find an iframe matching the specified pathname.
|
|
95
|
+
*
|
|
96
|
+
* @param pathname - The pathname to search for (default: "/listener")
|
|
97
|
+
* @returns The matching iframe window, or null if not found
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* // Find the default /listener iframe
|
|
102
|
+
* const listenerIframe = findIframeInOpener();
|
|
103
|
+
*
|
|
104
|
+
* // Find a custom iframe
|
|
105
|
+
* const customIframe = findIframeInOpener("/my-custom-iframe");
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export function findIframeInOpener(pathname = "/listener"): Window | null {
|
|
109
|
+
if (!window.opener) return null;
|
|
110
|
+
|
|
111
|
+
const frameCheck = (frame: Window) => {
|
|
112
|
+
try {
|
|
113
|
+
return (
|
|
114
|
+
frame.location.origin === window.location.origin &&
|
|
115
|
+
frame.location.pathname === pathname
|
|
116
|
+
);
|
|
117
|
+
} catch {
|
|
118
|
+
// Cross-origin frame, skip
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Check if the openner window is the right one
|
|
124
|
+
if (frameCheck(window.opener)) return window.opener;
|
|
125
|
+
|
|
126
|
+
// Search through frames in window.opener
|
|
127
|
+
try {
|
|
128
|
+
const frames = window.opener.frames;
|
|
129
|
+
for (let i = 0; i < frames.length; i++) {
|
|
130
|
+
if (frameCheck(frames[i])) {
|
|
131
|
+
return frames[i];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(
|
|
137
|
+
`[findIframeInOpener] Error finding iframe with pathname ${pathname}:`,
|
|
138
|
+
error
|
|
139
|
+
);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createIframe,
|
|
3
|
+
baseIframeProps,
|
|
4
|
+
findIframeInOpener,
|
|
5
|
+
} from "./iframeHelper";
|
|
6
|
+
export { compressJsonToB64 } from "./compression/compress";
|
|
7
|
+
export { decompressJsonFromB64 } from "./compression/decompress";
|
|
8
|
+
export { base64urlDecode, base64urlEncode } from "./compression/b64";
|
|
9
|
+
export { FrakContextManager } from "./FrakContext";
|
|
10
|
+
export { getSupportedCurrency } from "./getSupportedCurrency";
|
|
11
|
+
export { getSupportedLocale } from "./getSupportedLocale";
|
|
12
|
+
export { getCurrencyAmountKey } from "./getCurrencyAmountKey";
|
|
13
|
+
export { formatAmount } from "./formatAmount";
|
|
14
|
+
export { trackEvent } from "./trackEvent";
|
|
15
|
+
export { Deferred } from "@frak-labs/frame-connector";
|
|
16
|
+
export {
|
|
17
|
+
generateSsoUrl,
|
|
18
|
+
type CompressedSsoData,
|
|
19
|
+
type FullSsoParams,
|
|
20
|
+
type AppSpecificSsoMetadata,
|
|
21
|
+
} from "./sso";
|
package/src/utils/sso.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Hex } from "viem";
|
|
2
|
+
import type { PrepareSsoParamsType, SsoMetadata } from "../types";
|
|
3
|
+
import { compressJsonToB64 } from "./compression/compress";
|
|
4
|
+
|
|
5
|
+
export type AppSpecificSsoMetadata = SsoMetadata & {
|
|
6
|
+
name: string;
|
|
7
|
+
css?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The full SSO params that will be used for compression
|
|
12
|
+
*/
|
|
13
|
+
export type FullSsoParams = Omit<PrepareSsoParamsType, "metadata"> & {
|
|
14
|
+
metadata: AppSpecificSsoMetadata;
|
|
15
|
+
productId: Hex;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate SSO URL with compressed parameters
|
|
20
|
+
* This mirrors the wallet's getOpenSsoLink() function
|
|
21
|
+
*
|
|
22
|
+
* @param walletUrl - Base wallet URL (e.g., "https://wallet.frak.id")
|
|
23
|
+
* @param params - SSO parameters
|
|
24
|
+
* @param productId - Product identifier
|
|
25
|
+
* @param name - Application name
|
|
26
|
+
* @param css - Optional custom CSS
|
|
27
|
+
* @returns Complete SSO URL ready to open in popup or redirect
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const ssoUrl = generateSsoUrl(
|
|
32
|
+
* "https://wallet.frak.id",
|
|
33
|
+
* { metadata: { logoUrl: "..." }, directExit: true },
|
|
34
|
+
* "0x123...",
|
|
35
|
+
* "My App"
|
|
36
|
+
* );
|
|
37
|
+
* // Returns: https://wallet.frak.id/sso?p=<compressed_base64>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function generateSsoUrl(
|
|
41
|
+
walletUrl: string,
|
|
42
|
+
params: PrepareSsoParamsType,
|
|
43
|
+
productId: Hex,
|
|
44
|
+
name: string,
|
|
45
|
+
css?: string
|
|
46
|
+
): string {
|
|
47
|
+
// Build full params with app-specific metadata
|
|
48
|
+
const fullParams: FullSsoParams = {
|
|
49
|
+
redirectUrl: params.redirectUrl,
|
|
50
|
+
directExit: params.directExit,
|
|
51
|
+
lang: params.lang,
|
|
52
|
+
productId,
|
|
53
|
+
metadata: {
|
|
54
|
+
name,
|
|
55
|
+
css,
|
|
56
|
+
logoUrl: params.metadata?.logoUrl,
|
|
57
|
+
homepageLink: params.metadata?.homepageLink,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Compress params to minimal format
|
|
62
|
+
const compressedParam = ssoParamsToCompressed(fullParams);
|
|
63
|
+
|
|
64
|
+
// Encode to base64url
|
|
65
|
+
const compressedString = compressJsonToB64(compressedParam);
|
|
66
|
+
|
|
67
|
+
// Build URL matching wallet's expected format: /sso?p=<compressed>
|
|
68
|
+
const ssoUrl = new URL(walletUrl);
|
|
69
|
+
ssoUrl.pathname = "/sso";
|
|
70
|
+
ssoUrl.searchParams.set("p", compressedString);
|
|
71
|
+
|
|
72
|
+
return ssoUrl.toString();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Map full sso params to compressed sso params
|
|
77
|
+
* @param params
|
|
78
|
+
*/
|
|
79
|
+
function ssoParamsToCompressed(params: FullSsoParams) {
|
|
80
|
+
return {
|
|
81
|
+
r: params.redirectUrl,
|
|
82
|
+
d: params.directExit,
|
|
83
|
+
l: params.lang,
|
|
84
|
+
p: params.productId,
|
|
85
|
+
m: {
|
|
86
|
+
n: params.metadata?.name,
|
|
87
|
+
css: params.metadata?.css,
|
|
88
|
+
l: params.metadata?.logoUrl,
|
|
89
|
+
h: params.metadata?.homepageLink,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Type of compressed the sso data
|
|
96
|
+
*/
|
|
97
|
+
export type CompressedSsoData = {
|
|
98
|
+
// Potential id from backend
|
|
99
|
+
id?: Hex;
|
|
100
|
+
// redirect url
|
|
101
|
+
r?: string;
|
|
102
|
+
// direct exit
|
|
103
|
+
d?: boolean;
|
|
104
|
+
// language
|
|
105
|
+
l?: "en" | "fr";
|
|
106
|
+
// product id
|
|
107
|
+
p: Hex;
|
|
108
|
+
// metadata
|
|
109
|
+
m: {
|
|
110
|
+
// product name
|
|
111
|
+
n: string;
|
|
112
|
+
// custom css
|
|
113
|
+
css?: string;
|
|
114
|
+
// logo
|
|
115
|
+
l?: string;
|
|
116
|
+
// home page link
|
|
117
|
+
h?: string;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { RpcClient } from "@frak-labs/frame-connector";
|
|
2
|
+
import type { FrakLifecycleEvent } from "../types";
|
|
3
|
+
import type { IFrameRpcSchema } from "../types/rpc";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Listen for SSO redirect with compressed data in URL
|
|
7
|
+
* Forwards compressed data to iframe via lifecycle event
|
|
8
|
+
* Cleans URL immediately after detection
|
|
9
|
+
*
|
|
10
|
+
* Performance: One-shot URL check, no polling, no re-renders
|
|
11
|
+
*
|
|
12
|
+
* @param rpcClient - RPC client instance to send lifecycle events
|
|
13
|
+
* @param waitForConnection - Promise that resolves when iframe is connected
|
|
14
|
+
*/
|
|
15
|
+
export function setupSsoUrlListener(
|
|
16
|
+
rpcClient: RpcClient<IFrameRpcSchema, FrakLifecycleEvent>,
|
|
17
|
+
waitForConnection: Promise<boolean>
|
|
18
|
+
): void {
|
|
19
|
+
if (typeof window === "undefined") {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// One-shot URL check - no need for MutationObserver or polling
|
|
24
|
+
const url = new URL(window.location.href);
|
|
25
|
+
const compressedSso = url.searchParams.get("sso");
|
|
26
|
+
|
|
27
|
+
// Early return if no SSO parameter
|
|
28
|
+
if (!compressedSso) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Forward compressed data directly to iframe (no decompression on SDK side)
|
|
33
|
+
// Iframe will decompress and process
|
|
34
|
+
waitForConnection
|
|
35
|
+
.then(() => {
|
|
36
|
+
// Send lifecycle event with compressed string
|
|
37
|
+
// This is a one-way notification, no response expected
|
|
38
|
+
rpcClient.sendLifecycle({
|
|
39
|
+
clientLifecycle: "sso-redirect-complete",
|
|
40
|
+
data: { compressed: compressedSso },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log(
|
|
44
|
+
"[SSO URL Listener] Forwarded compressed SSO data to iframe"
|
|
45
|
+
);
|
|
46
|
+
})
|
|
47
|
+
.catch((error) => {
|
|
48
|
+
console.error(
|
|
49
|
+
"[SSO URL Listener] Failed to forward SSO data:",
|
|
50
|
+
error
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Clean URL immediately to prevent exposure in browser history
|
|
55
|
+
// Use replaceState to avoid navigation/re-render
|
|
56
|
+
url.searchParams.delete("sso");
|
|
57
|
+
window.history.replaceState({}, "", url.toString());
|
|
58
|
+
|
|
59
|
+
console.log("[SSO URL Listener] SSO parameter detected and URL cleaned");
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FrakClient } from "../types";
|
|
2
|
+
|
|
3
|
+
export type FrakEvent =
|
|
4
|
+
| "share_button_clicked"
|
|
5
|
+
| "wallet_button_clicked"
|
|
6
|
+
| "share_modal_error"
|
|
7
|
+
| "user_referred";
|
|
8
|
+
|
|
9
|
+
type EventProps = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
export function trackEvent(
|
|
12
|
+
client: FrakClient | undefined,
|
|
13
|
+
event: FrakEvent,
|
|
14
|
+
props: EventProps = {}
|
|
15
|
+
): void {
|
|
16
|
+
if (!client) {
|
|
17
|
+
console.debug("[Frak] No client provided, skipping event tracking");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
client.openPanel?.track(event, props);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.debug("[Frak] Failed to track event:", event, e);
|
|
25
|
+
}
|
|
26
|
+
}
|