@frak-labs/core-sdk 0.0.19-beta.f259d7fc → 0.1.0-beta.263acd1e
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.iife.js +14 -0
- package/dist/actions-CEEObPYc.js +1 -0
- package/dist/actions-DbQhWYx8.cjs +1 -0
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +3 -1400
- package/dist/actions.d.ts +3 -1400
- package/dist/actions.js +1 -1
- package/dist/bundle.cjs +1 -13
- package/dist/bundle.d.cts +6 -1927
- package/dist/bundle.d.ts +6 -1927
- package/dist/bundle.js +1 -13
- package/dist/index-7OZ39x1U.d.ts +195 -0
- package/dist/index-C6FxkWPC.d.cts +511 -0
- package/dist/index-UFX7xCg3.d.ts +351 -0
- package/dist/index-d8xS4ryI.d.ts +511 -0
- package/dist/index-p4FqSp8z.d.cts +351 -0
- package/dist/index-zDq-VlKx.d.cts +195 -0
- package/dist/index.cjs +1 -13
- package/dist/index.d.cts +4 -1269
- package/dist/index.d.ts +4 -1269
- package/dist/index.js +1 -13
- package/dist/interaction-DMJ3ZfaF.d.cts +45 -0
- package/dist/interaction-KX1h9a7V.d.ts +45 -0
- package/dist/interactions-DnfM3oe0.js +1 -0
- package/dist/interactions-EIXhNLf6.cjs +1 -0
- package/dist/interactions.cjs +1 -1
- package/dist/interactions.d.cts +2 -182
- package/dist/interactions.d.ts +2 -182
- package/dist/interactions.js +1 -1
- package/dist/openSso-D--Airj6.d.cts +1018 -0
- package/dist/openSso-DsKJ4y0j.d.ts +1018 -0
- package/dist/productTypes-BUkXJKZ7.cjs +1 -0
- package/dist/productTypes-CGb1MmBF.js +1 -0
- package/dist/src-B_xO0AR6.cjs +13 -0
- package/dist/src-D2d52OZa.js +13 -0
- package/dist/trackEvent-CHnYa85W.js +1 -0
- package/dist/trackEvent-GuQm_1Nm.cjs +1 -0
- package/package.json +24 -19
- package/src/actions/displayEmbeddedWallet.test.ts +194 -0
- package/src/actions/displayEmbeddedWallet.ts +20 -0
- package/src/actions/displayModal.test.ts +387 -0
- package/src/actions/displayModal.ts +131 -0
- package/src/actions/getProductInformation.test.ts +133 -0
- package/src/actions/getProductInformation.ts +14 -0
- package/src/actions/index.ts +29 -0
- package/src/actions/openSso.test.ts +407 -0
- package/src/actions/openSso.ts +116 -0
- package/src/actions/prepareSso.test.ts +223 -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.test.ts +219 -0
- package/src/actions/sendInteraction.ts +32 -0
- package/src/actions/trackPurchaseStatus.test.ts +287 -0
- package/src/actions/trackPurchaseStatus.ts +53 -0
- package/src/actions/watchWalletStatus.test.ts +372 -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.test.ts +343 -0
- package/src/clients/setupClient.ts +73 -0
- package/src/clients/transports/iframeLifecycleManager.test.ts +399 -0
- package/src/clients/transports/iframeLifecycleManager.ts +90 -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 +101 -0
- package/src/interactions/index.ts +5 -0
- package/src/interactions/pressEncoder.test.ts +215 -0
- package/src/interactions/pressEncoder.ts +53 -0
- package/src/interactions/purchaseEncoder.test.ts +291 -0
- package/src/interactions/purchaseEncoder.ts +99 -0
- package/src/interactions/referralEncoder.test.ts +170 -0
- package/src/interactions/referralEncoder.ts +47 -0
- package/src/interactions/retailEncoder.test.ts +107 -0
- package/src/interactions/retailEncoder.ts +37 -0
- package/src/interactions/webshopEncoder.test.ts +56 -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 +71 -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.test.ts +407 -0
- package/src/utils/FrakContext.ts +158 -0
- package/src/utils/compression/b64.test.ts +181 -0
- package/src/utils/compression/b64.ts +29 -0
- package/src/utils/compression/compress.test.ts +123 -0
- package/src/utils/compression/compress.ts +11 -0
- package/src/utils/compression/decompress.test.ts +145 -0
- package/src/utils/compression/decompress.ts +11 -0
- package/src/utils/compression/index.ts +3 -0
- package/src/utils/computeProductId.test.ts +80 -0
- package/src/utils/computeProductId.ts +11 -0
- package/src/utils/constants.test.ts +23 -0
- package/src/utils/constants.ts +4 -0
- package/src/utils/formatAmount.test.ts +113 -0
- package/src/utils/formatAmount.ts +18 -0
- package/src/utils/getCurrencyAmountKey.test.ts +44 -0
- package/src/utils/getCurrencyAmountKey.ts +15 -0
- package/src/utils/getSupportedCurrency.test.ts +51 -0
- package/src/utils/getSupportedCurrency.ts +14 -0
- package/src/utils/getSupportedLocale.test.ts +64 -0
- package/src/utils/getSupportedLocale.ts +16 -0
- package/src/utils/iframeHelper.test.ts +450 -0
- package/src/utils/iframeHelper.ts +143 -0
- package/src/utils/index.ts +21 -0
- package/src/utils/sso.test.ts +361 -0
- package/src/utils/sso.ts +119 -0
- package/src/utils/ssoUrlListener.ts +60 -0
- package/src/utils/trackEvent.test.ts +162 -0
- package/src/utils/trackEvent.ts +26 -0
- package/cdn/bundle.js +0 -19
- package/cdn/bundle.js.LICENSE.txt +0 -10
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FrakClient,
|
|
3
|
+
ModalRpcMetadata,
|
|
4
|
+
SendTransactionModalStepType,
|
|
5
|
+
SendTransactionReturnType,
|
|
6
|
+
} from "../../types";
|
|
7
|
+
import { displayModal } from "../displayModal";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parameters to directly show a modal used to send a transaction
|
|
11
|
+
* @inline
|
|
12
|
+
*/
|
|
13
|
+
export type SendTransactionParams = {
|
|
14
|
+
/**
|
|
15
|
+
* The transaction to be sent (either a single tx or multiple ones)
|
|
16
|
+
*/
|
|
17
|
+
tx: SendTransactionModalStepType["params"]["tx"];
|
|
18
|
+
/**
|
|
19
|
+
* Custom metadata to be passed to the modal
|
|
20
|
+
*/
|
|
21
|
+
metadata?: ModalRpcMetadata;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Function used to send a user transaction, simple wrapper around the displayModal function to ease the send transaction process
|
|
26
|
+
* @param client - The current Frak Client
|
|
27
|
+
* @param args - The parameters
|
|
28
|
+
* @returns The hash of the transaction that was sent in a promise
|
|
29
|
+
*
|
|
30
|
+
* @description This function will display a modal to the user with the provided transaction and metadata.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const { hash } = await sendTransaction(frakConfig, {
|
|
34
|
+
* tx: {
|
|
35
|
+
* to: "0xdeadbeef",
|
|
36
|
+
* value: toHex(100n),
|
|
37
|
+
* },
|
|
38
|
+
* metadata: {
|
|
39
|
+
* header: {
|
|
40
|
+
* title: "Sending eth",
|
|
41
|
+
* },
|
|
42
|
+
* context: "Send 100wei to 0xdeadbeef",
|
|
43
|
+
* },
|
|
44
|
+
* });
|
|
45
|
+
* console.log("Transaction hash:", hash);
|
|
46
|
+
*/
|
|
47
|
+
export async function sendTransaction(
|
|
48
|
+
client: FrakClient,
|
|
49
|
+
{ tx, metadata }: SendTransactionParams
|
|
50
|
+
): Promise<SendTransactionReturnType> {
|
|
51
|
+
// Trigger a modal with login options
|
|
52
|
+
const result = await displayModal(client, {
|
|
53
|
+
metadata,
|
|
54
|
+
steps: {
|
|
55
|
+
login: {},
|
|
56
|
+
sendTransaction: { tx },
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Return the tx result only
|
|
61
|
+
return result.sendTransaction;
|
|
62
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { generateSiweNonce } from "viem/siwe";
|
|
2
|
+
import type {
|
|
3
|
+
FrakClient,
|
|
4
|
+
ModalRpcMetadata,
|
|
5
|
+
SiweAuthenticateReturnType,
|
|
6
|
+
SiweAuthenticationParams,
|
|
7
|
+
} from "../../types";
|
|
8
|
+
import { displayModal } from "../displayModal";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parameter used to directly show a modal used to authenticate with SIWE
|
|
12
|
+
* @inline
|
|
13
|
+
*/
|
|
14
|
+
export type SiweAuthenticateModalParams = {
|
|
15
|
+
/**
|
|
16
|
+
* Partial SIWE params, since we can rebuild them from the SDK if they are empty
|
|
17
|
+
*
|
|
18
|
+
* If no parameters provider, some fields will be recomputed from the current configuration and environment.
|
|
19
|
+
* - `statement` will be set to a default value
|
|
20
|
+
* - `nonce` will be generated
|
|
21
|
+
* - `uri` will be set to the current domain
|
|
22
|
+
* - `version` will be set to "1"
|
|
23
|
+
* - `domain` will be set to the current window domain
|
|
24
|
+
*
|
|
25
|
+
* @default {}
|
|
26
|
+
*/
|
|
27
|
+
siwe?: Partial<SiweAuthenticationParams>;
|
|
28
|
+
/**
|
|
29
|
+
* Custom metadata to be passed to the modal
|
|
30
|
+
*/
|
|
31
|
+
metadata?: ModalRpcMetadata;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Function used to launch a siwe authentication
|
|
36
|
+
* @param client - The current Frak Client
|
|
37
|
+
* @param args - The parameters
|
|
38
|
+
* @returns The SIWE authentication result (message + signature) in a promise
|
|
39
|
+
*
|
|
40
|
+
* @description This function will display a modal to the user with the provided SIWE parameters and metadata.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* import { siweAuthenticate } from "@frak-labs/core-sdk/actions";
|
|
44
|
+
* import { parseSiweMessage } from "viem/siwe";
|
|
45
|
+
*
|
|
46
|
+
* const { signature, message } = await siweAuthenticate(frakConfig, {
|
|
47
|
+
* siwe: {
|
|
48
|
+
* statement: "Sign in to My App",
|
|
49
|
+
* domain: "my-app.com",
|
|
50
|
+
* expirationTimeTimestamp: Date.now() + 1000 * 60 * 5,
|
|
51
|
+
* },
|
|
52
|
+
* metadata: {
|
|
53
|
+
* header: {
|
|
54
|
+
* title: "Sign in",
|
|
55
|
+
* },
|
|
56
|
+
* context: "Sign in to My App",
|
|
57
|
+
* },
|
|
58
|
+
* });
|
|
59
|
+
* console.log("Parsed final message:", parseSiweMessage(message));
|
|
60
|
+
* console.log("Siwe signature:", signature);
|
|
61
|
+
*/
|
|
62
|
+
export async function siweAuthenticate(
|
|
63
|
+
client: FrakClient,
|
|
64
|
+
{ siwe, metadata }: SiweAuthenticateModalParams
|
|
65
|
+
): Promise<SiweAuthenticateReturnType> {
|
|
66
|
+
const effectiveDomain = client.config?.domain ?? window.location.host;
|
|
67
|
+
const realStatement =
|
|
68
|
+
siwe?.statement ??
|
|
69
|
+
`I confirm that I want to use my Frak wallet on: ${client.config.metadata.name}`;
|
|
70
|
+
|
|
71
|
+
// Fill up the siwe request params
|
|
72
|
+
const builtSiwe: SiweAuthenticationParams = {
|
|
73
|
+
...siwe,
|
|
74
|
+
statement: realStatement,
|
|
75
|
+
nonce: siwe?.nonce ?? generateSiweNonce(),
|
|
76
|
+
uri: siwe?.uri ?? `https://${effectiveDomain}`,
|
|
77
|
+
version: siwe?.version ?? "1",
|
|
78
|
+
domain: effectiveDomain,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Trigger a modal with login options
|
|
82
|
+
const result = await displayModal(client, {
|
|
83
|
+
metadata,
|
|
84
|
+
steps: {
|
|
85
|
+
login: {},
|
|
86
|
+
siweAuthenticate: {
|
|
87
|
+
siwe: builtSiwe,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Return the SIWE result only
|
|
93
|
+
return result.siweAuthenticate;
|
|
94
|
+
}
|
package/src/bundle.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FrakRpcError,
|
|
3
|
+
type RpcMessage,
|
|
4
|
+
type RpcResponse,
|
|
5
|
+
} from "@frak-labs/frame-connector";
|
|
6
|
+
import type { FrakWalletSdkConfig } from "../types";
|
|
7
|
+
|
|
8
|
+
type IframeStatus = {
|
|
9
|
+
loading: boolean;
|
|
10
|
+
url: string | null;
|
|
11
|
+
readyState: number;
|
|
12
|
+
contentWindow: boolean;
|
|
13
|
+
isConnected: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type DebugInfo = {
|
|
17
|
+
timestamp: string;
|
|
18
|
+
encodedUrl: string;
|
|
19
|
+
navigatorInfo: string;
|
|
20
|
+
encodedConfig: string;
|
|
21
|
+
iframeStatus: string;
|
|
22
|
+
lastRequest: string;
|
|
23
|
+
lastResponse: string;
|
|
24
|
+
clientStatus: string;
|
|
25
|
+
error: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type NavigatorInfo = {
|
|
29
|
+
userAgent: string;
|
|
30
|
+
language: string;
|
|
31
|
+
onLine: boolean;
|
|
32
|
+
screenWidth: number;
|
|
33
|
+
screenHeight: number;
|
|
34
|
+
pixelRatio: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** @ignore */
|
|
38
|
+
export class DebugInfoGatherer {
|
|
39
|
+
private config?: FrakWalletSdkConfig;
|
|
40
|
+
private iframe?: HTMLIFrameElement;
|
|
41
|
+
private isSetupDone = false;
|
|
42
|
+
private lastResponse: null | {
|
|
43
|
+
message: RpcMessage;
|
|
44
|
+
response: RpcResponse;
|
|
45
|
+
timestamp: number;
|
|
46
|
+
} = null;
|
|
47
|
+
private lastRequest: null | {
|
|
48
|
+
event: RpcMessage;
|
|
49
|
+
timestamp: number;
|
|
50
|
+
} = null;
|
|
51
|
+
|
|
52
|
+
constructor(config?: FrakWalletSdkConfig, iframe?: HTMLIFrameElement) {
|
|
53
|
+
this.config = config;
|
|
54
|
+
this.iframe = iframe;
|
|
55
|
+
this.lastRequest = null;
|
|
56
|
+
this.lastResponse = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Update communication logs
|
|
60
|
+
public setLastResponse(message: RpcMessage, response: RpcResponse) {
|
|
61
|
+
this.lastResponse = {
|
|
62
|
+
message,
|
|
63
|
+
response,
|
|
64
|
+
timestamp: Date.now(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
public setLastRequest(event: RpcMessage) {
|
|
68
|
+
this.lastRequest = { event, timestamp: Date.now() };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Update connection status
|
|
72
|
+
public updateSetupStatus(status: boolean) {
|
|
73
|
+
this.isSetupDone = status;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private base64Encode(data: object): string {
|
|
77
|
+
try {
|
|
78
|
+
return btoa(JSON.stringify(data));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.warn("Failed to encode debug data", err);
|
|
81
|
+
return btoa("Failed to encode data");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extract information from the iframe status
|
|
87
|
+
*/
|
|
88
|
+
private getIframeStatus(): IframeStatus | null {
|
|
89
|
+
if (!this.iframe) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
loading: this.iframe.hasAttribute("loading"),
|
|
94
|
+
url: this.iframe.src,
|
|
95
|
+
readyState: this.iframe.contentDocument?.readyState
|
|
96
|
+
? this.iframe.contentDocument.readyState === "complete"
|
|
97
|
+
? 1
|
|
98
|
+
: 0
|
|
99
|
+
: -1,
|
|
100
|
+
contentWindow: !!this.iframe.contentWindow,
|
|
101
|
+
isConnected: this.iframe.isConnected,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private getNavigatorInfo(): NavigatorInfo | null {
|
|
106
|
+
if (!navigator) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
userAgent: navigator.userAgent,
|
|
111
|
+
language: navigator.language,
|
|
112
|
+
onLine: navigator.onLine,
|
|
113
|
+
screenWidth: window.screen.width,
|
|
114
|
+
screenHeight: window.screen.height,
|
|
115
|
+
pixelRatio: window.devicePixelRatio,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private gatherDebugInfo(error: Error | unknown): DebugInfo {
|
|
120
|
+
const iframeStatus = this.getIframeStatus();
|
|
121
|
+
const navigatorInfo = this.getNavigatorInfo();
|
|
122
|
+
|
|
123
|
+
// Format the error in a readable format
|
|
124
|
+
let formattedError = "Unknown";
|
|
125
|
+
if (error instanceof FrakRpcError) {
|
|
126
|
+
formattedError = `FrakRpcError: ${error.code} '${error.message}'`;
|
|
127
|
+
} else if (error instanceof Error) {
|
|
128
|
+
formattedError = error.message;
|
|
129
|
+
} else if (typeof error === "string") {
|
|
130
|
+
formattedError = error;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Craft the debug info
|
|
134
|
+
const debugInfo: DebugInfo = {
|
|
135
|
+
timestamp: new Date().toISOString(),
|
|
136
|
+
encodedUrl: btoa(window.location.href),
|
|
137
|
+
encodedConfig: this.config
|
|
138
|
+
? this.base64Encode(this.config)
|
|
139
|
+
: "no-config",
|
|
140
|
+
navigatorInfo: navigatorInfo
|
|
141
|
+
? this.base64Encode(navigatorInfo)
|
|
142
|
+
: "no-navigator",
|
|
143
|
+
iframeStatus: iframeStatus
|
|
144
|
+
? this.base64Encode(iframeStatus)
|
|
145
|
+
: "not-iframe",
|
|
146
|
+
lastRequest: this.lastRequest
|
|
147
|
+
? this.base64Encode(this.lastRequest)
|
|
148
|
+
: "No Frak request logged",
|
|
149
|
+
lastResponse: this.lastResponse
|
|
150
|
+
? this.base64Encode(this.lastResponse)
|
|
151
|
+
: "No Frak response logged",
|
|
152
|
+
clientStatus: this.isSetupDone ? "setup" : "not-setup",
|
|
153
|
+
error: formattedError,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return debugInfo;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public static empty(): DebugInfoGatherer {
|
|
160
|
+
return new DebugInfoGatherer();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format Frak debug information
|
|
165
|
+
*/
|
|
166
|
+
public formatDebugInfo(error: Error | unknown | string): string {
|
|
167
|
+
const debugInfo = this.gatherDebugInfo(error);
|
|
168
|
+
return `
|
|
169
|
+
Debug Information:
|
|
170
|
+
-----------------
|
|
171
|
+
Timestamp: ${debugInfo.timestamp}
|
|
172
|
+
URL: ${debugInfo.encodedUrl}
|
|
173
|
+
Config: ${debugInfo.encodedConfig}
|
|
174
|
+
Navigator Info: ${debugInfo.navigatorInfo}
|
|
175
|
+
IFrame Status: ${debugInfo.iframeStatus}
|
|
176
|
+
Last Request: ${debugInfo.lastRequest}
|
|
177
|
+
Last Response: ${debugInfo.lastResponse}
|
|
178
|
+
Client Status: ${debugInfo.clientStatus}
|
|
179
|
+
Error: ${debugInfo.error}
|
|
180
|
+
`.trim();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRpcClient,
|
|
3
|
+
FrakRpcError,
|
|
4
|
+
type RpcClient,
|
|
5
|
+
RpcErrorCodes,
|
|
6
|
+
} from "@frak-labs/frame-connector";
|
|
7
|
+
import { createClientCompressionMiddleware } from "@frak-labs/frame-connector/middleware";
|
|
8
|
+
import { OpenPanel } from "@openpanel/web";
|
|
9
|
+
import type { FrakLifecycleEvent } from "../types";
|
|
10
|
+
import type { FrakClient } from "../types/client";
|
|
11
|
+
import type { FrakWalletSdkConfig } from "../types/config";
|
|
12
|
+
import type { IFrameRpcSchema } from "../types/rpc";
|
|
13
|
+
import { BACKUP_KEY } from "../utils/constants";
|
|
14
|
+
import { setupSsoUrlListener } from "../utils/ssoUrlListener";
|
|
15
|
+
import { DebugInfoGatherer } from "./DebugInfo";
|
|
16
|
+
import {
|
|
17
|
+
createIFrameLifecycleManager,
|
|
18
|
+
type IframeLifecycleManager,
|
|
19
|
+
} from "./transports/iframeLifecycleManager";
|
|
20
|
+
|
|
21
|
+
type SdkRpcClient = RpcClient<IFrameRpcSchema, FrakLifecycleEvent>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a new iframe Frak client
|
|
25
|
+
* @param args
|
|
26
|
+
* @param args.config - The configuration to use for the Frak Wallet SDK
|
|
27
|
+
* @param args.iframe - The iframe to use for the communication
|
|
28
|
+
* @returns The created Frak Client
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const frakConfig: FrakWalletSdkConfig = {
|
|
32
|
+
* metadata: {
|
|
33
|
+
* name: "My app title",
|
|
34
|
+
* },
|
|
35
|
+
* }
|
|
36
|
+
* const iframe = await createIframe({ config: frakConfig });
|
|
37
|
+
* const client = createIFrameFrakClient({ config: frakConfig, iframe });
|
|
38
|
+
*/
|
|
39
|
+
export function createIFrameFrakClient({
|
|
40
|
+
config,
|
|
41
|
+
iframe,
|
|
42
|
+
}: {
|
|
43
|
+
config: FrakWalletSdkConfig;
|
|
44
|
+
iframe: HTMLIFrameElement;
|
|
45
|
+
}): FrakClient {
|
|
46
|
+
const frakWalletUrl = config?.walletUrl ?? "https://wallet.frak.id";
|
|
47
|
+
|
|
48
|
+
// Create lifecycle manager
|
|
49
|
+
const lifecycleManager = createIFrameLifecycleManager({ iframe });
|
|
50
|
+
|
|
51
|
+
// Create our debug info gatherer
|
|
52
|
+
const debugInfo = new DebugInfoGatherer(config, iframe);
|
|
53
|
+
|
|
54
|
+
// Validate iframe
|
|
55
|
+
if (!iframe.contentWindow) {
|
|
56
|
+
throw new FrakRpcError(
|
|
57
|
+
RpcErrorCodes.configError,
|
|
58
|
+
"The iframe does not have a content window"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Create RPC client with middleware and lifecycle handlers
|
|
63
|
+
const rpcClient = createRpcClient<IFrameRpcSchema, FrakLifecycleEvent>({
|
|
64
|
+
emittingTransport: iframe.contentWindow,
|
|
65
|
+
listeningTransport: window,
|
|
66
|
+
targetOrigin: frakWalletUrl,
|
|
67
|
+
// Add compression middleware to handle request/response compression
|
|
68
|
+
middleware: [
|
|
69
|
+
// Ensure we are connected before sending request
|
|
70
|
+
{
|
|
71
|
+
async onRequest(_message, ctx) {
|
|
72
|
+
// Ensure the iframe is connected
|
|
73
|
+
const isConnected = await lifecycleManager.isConnected;
|
|
74
|
+
if (!isConnected) {
|
|
75
|
+
throw new FrakRpcError(
|
|
76
|
+
RpcErrorCodes.clientNotConnected,
|
|
77
|
+
"The iframe provider isn't connected yet"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return ctx;
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
createClientCompressionMiddleware(),
|
|
84
|
+
// Save debug info
|
|
85
|
+
{
|
|
86
|
+
onRequest(message, ctx) {
|
|
87
|
+
debugInfo.setLastRequest(message);
|
|
88
|
+
return ctx;
|
|
89
|
+
},
|
|
90
|
+
onResponse(message, response) {
|
|
91
|
+
debugInfo.setLastResponse(message, response);
|
|
92
|
+
return response;
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
// Add lifecycle handlers to process iframe lifecycle events
|
|
97
|
+
lifecycleHandlers: {
|
|
98
|
+
iframeLifecycle: async (event, _context) => {
|
|
99
|
+
// Delegate to lifecycle manager (cast for type compatibility)
|
|
100
|
+
await lifecycleManager.handleEvent(event);
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Setup heartbeat
|
|
106
|
+
const stopHeartbeat = setupHeartbeat(rpcClient, lifecycleManager);
|
|
107
|
+
|
|
108
|
+
// Build our destroy function
|
|
109
|
+
const destroy = async () => {
|
|
110
|
+
// Stop heartbeat
|
|
111
|
+
stopHeartbeat();
|
|
112
|
+
// Cleanup the RPC client
|
|
113
|
+
rpcClient.cleanup();
|
|
114
|
+
// Remove the iframe
|
|
115
|
+
iframe.remove();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Init open panel
|
|
119
|
+
let openPanel: OpenPanel | undefined;
|
|
120
|
+
if (
|
|
121
|
+
process.env.OPEN_PANEL_API_URL &&
|
|
122
|
+
process.env.OPEN_PANEL_SDK_CLIENT_ID
|
|
123
|
+
) {
|
|
124
|
+
console.log("[Frak SDK] Initializing OpenPanel");
|
|
125
|
+
openPanel = new OpenPanel({
|
|
126
|
+
apiUrl: process.env.OPEN_PANEL_API_URL,
|
|
127
|
+
clientId: process.env.OPEN_PANEL_SDK_CLIENT_ID,
|
|
128
|
+
trackScreenViews: true,
|
|
129
|
+
trackOutgoingLinks: true,
|
|
130
|
+
trackAttributes: false,
|
|
131
|
+
// We use a filter to ensure we got the open panel instance initialized
|
|
132
|
+
// A bit hacky, but this way we are sure that we got everything needed for the first event ever sent
|
|
133
|
+
filter: ({ type, payload }) => {
|
|
134
|
+
if (type !== "track") return true;
|
|
135
|
+
if (!payload?.properties) return true;
|
|
136
|
+
|
|
137
|
+
// Check if we we got the properties once initialized
|
|
138
|
+
if (!("sdkVersion" in payload.properties)) {
|
|
139
|
+
payload.properties = {
|
|
140
|
+
...payload.properties,
|
|
141
|
+
sdkVersion: process.env.SDK_VERSION,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return true;
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
openPanel.setGlobalProperties({
|
|
149
|
+
sdkVersion: process.env.SDK_VERSION,
|
|
150
|
+
});
|
|
151
|
+
openPanel.init();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Perform the post connection setup
|
|
155
|
+
const waitForSetup = postConnectionSetup({
|
|
156
|
+
config,
|
|
157
|
+
rpcClient,
|
|
158
|
+
lifecycleManager,
|
|
159
|
+
}).then(() => debugInfo.updateSetupStatus(true));
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
config,
|
|
163
|
+
debugInfo,
|
|
164
|
+
waitForConnection: lifecycleManager.isConnected,
|
|
165
|
+
waitForSetup,
|
|
166
|
+
request: rpcClient.request,
|
|
167
|
+
listenerRequest: rpcClient.listen,
|
|
168
|
+
destroy,
|
|
169
|
+
openPanel,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Setup the heartbeat
|
|
175
|
+
* @param rpcClient - RPC client to send lifecycle events
|
|
176
|
+
* @param lifecycleManager - Lifecycle manager to track connection
|
|
177
|
+
*/
|
|
178
|
+
function setupHeartbeat(
|
|
179
|
+
rpcClient: SdkRpcClient,
|
|
180
|
+
lifecycleManager: IframeLifecycleManager
|
|
181
|
+
) {
|
|
182
|
+
const HEARTBEAT_INTERVAL = 1_000; // Send heartbeat every 100ms until we are connected
|
|
183
|
+
const HEARTBEAT_TIMEOUT = 30_000; // 30 seconds timeout
|
|
184
|
+
let heartbeatInterval: NodeJS.Timeout;
|
|
185
|
+
let timeoutId: NodeJS.Timeout;
|
|
186
|
+
|
|
187
|
+
const sendHeartbeat = () =>
|
|
188
|
+
rpcClient.sendLifecycle({
|
|
189
|
+
clientLifecycle: "heartbeat",
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Start sending heartbeats
|
|
193
|
+
async function startHeartbeat() {
|
|
194
|
+
sendHeartbeat(); // Send initial heartbeat
|
|
195
|
+
heartbeatInterval = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL);
|
|
196
|
+
|
|
197
|
+
// Set up timeout
|
|
198
|
+
timeoutId = setTimeout(() => {
|
|
199
|
+
stopHeartbeat();
|
|
200
|
+
console.log("Heartbeat timeout: connection failed");
|
|
201
|
+
}, HEARTBEAT_TIMEOUT);
|
|
202
|
+
|
|
203
|
+
// Once connected, stop it
|
|
204
|
+
await lifecycleManager.isConnected;
|
|
205
|
+
|
|
206
|
+
// We are now connected, stop the heartbeat
|
|
207
|
+
stopHeartbeat();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Stop sending heartbeats
|
|
211
|
+
function stopHeartbeat() {
|
|
212
|
+
if (heartbeatInterval) {
|
|
213
|
+
clearInterval(heartbeatInterval);
|
|
214
|
+
}
|
|
215
|
+
if (timeoutId) {
|
|
216
|
+
clearTimeout(timeoutId);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
startHeartbeat();
|
|
221
|
+
|
|
222
|
+
// Return cleanup function
|
|
223
|
+
return stopHeartbeat;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Perform the post connection setup
|
|
228
|
+
* @param config - SDK configuration
|
|
229
|
+
* @param rpcClient - RPC client to send lifecycle events
|
|
230
|
+
* @param lifecycleManager - Lifecycle manager to track connection
|
|
231
|
+
*/
|
|
232
|
+
async function postConnectionSetup({
|
|
233
|
+
config,
|
|
234
|
+
rpcClient,
|
|
235
|
+
lifecycleManager,
|
|
236
|
+
}: {
|
|
237
|
+
config: FrakWalletSdkConfig;
|
|
238
|
+
rpcClient: SdkRpcClient;
|
|
239
|
+
lifecycleManager: IframeLifecycleManager;
|
|
240
|
+
}): Promise<void> {
|
|
241
|
+
// Wait for the handler to be connected
|
|
242
|
+
await lifecycleManager.isConnected;
|
|
243
|
+
|
|
244
|
+
// Setup SSO URL listener to detect and forward SSO redirects
|
|
245
|
+
// This checks for ?sso= parameter and forwards compressed data to iframe
|
|
246
|
+
setupSsoUrlListener(rpcClient, lifecycleManager.isConnected);
|
|
247
|
+
|
|
248
|
+
// Push raw CSS if needed
|
|
249
|
+
async function pushCss() {
|
|
250
|
+
const cssLink = config.customizations?.css;
|
|
251
|
+
if (!cssLink) return;
|
|
252
|
+
|
|
253
|
+
const message = {
|
|
254
|
+
clientLifecycle: "modal-css" as const,
|
|
255
|
+
data: { cssLink },
|
|
256
|
+
};
|
|
257
|
+
rpcClient.sendLifecycle(message);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Push i18n if needed
|
|
261
|
+
async function pushI18n() {
|
|
262
|
+
const i18n = config.customizations?.i18n;
|
|
263
|
+
if (!i18n) return;
|
|
264
|
+
|
|
265
|
+
const message = {
|
|
266
|
+
clientLifecycle: "modal-i18n" as const,
|
|
267
|
+
data: { i18n },
|
|
268
|
+
};
|
|
269
|
+
rpcClient.sendLifecycle(message);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Push local backup if needed
|
|
273
|
+
async function pushBackup() {
|
|
274
|
+
if (typeof window === "undefined") return;
|
|
275
|
+
|
|
276
|
+
const backup = window.localStorage.getItem(BACKUP_KEY);
|
|
277
|
+
if (!backup) return;
|
|
278
|
+
|
|
279
|
+
const message = {
|
|
280
|
+
clientLifecycle: "restore-backup" as const,
|
|
281
|
+
data: { backup },
|
|
282
|
+
};
|
|
283
|
+
rpcClient.sendLifecycle(message);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await Promise.allSettled([pushCss(), pushI18n(), pushBackup()]);
|
|
287
|
+
}
|