@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,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
|
+
FrakRpcError,
|
|
3
|
+
type RpcClient,
|
|
4
|
+
RpcErrorCodes,
|
|
5
|
+
createRpcClient,
|
|
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
|
+
type IframeLifecycleManager,
|
|
18
|
+
createIFrameLifecycleManager,
|
|
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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createIFrameFrakClient } from "../clients";
|
|
2
|
+
import type { FrakClient, FrakWalletSdkConfig } from "../types";
|
|
3
|
+
import { createIframe, getSupportedCurrency } from "../utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Directly setup the Frak client with an iframe
|
|
7
|
+
* Return when the FrakClient is ready (setup and communication estbalished with the wallet)
|
|
8
|
+
*
|
|
9
|
+
* @param config - The configuration to use for the Frak Wallet SDK
|
|
10
|
+
* @returns a Promise with the Frak Client
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const frakConfig: FrakWalletSdkConfig = {
|
|
14
|
+
* metadata: {
|
|
15
|
+
* name: "My app title",
|
|
16
|
+
* },
|
|
17
|
+
* }
|
|
18
|
+
* const client = await setupClient({ config: frakConfig });
|
|
19
|
+
*/
|
|
20
|
+
export async function setupClient({
|
|
21
|
+
config,
|
|
22
|
+
}: { config: FrakWalletSdkConfig }): Promise<FrakClient | undefined> {
|
|
23
|
+
// Prepare the config
|
|
24
|
+
const preparedConfig = prepareConfig(config);
|
|
25
|
+
|
|
26
|
+
// Create our iframe
|
|
27
|
+
const iframe = await createIframe({
|
|
28
|
+
config: preparedConfig,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!iframe) {
|
|
32
|
+
console.error("Failed to create iframe");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Create our client
|
|
37
|
+
const client = createIFrameFrakClient({
|
|
38
|
+
config: preparedConfig,
|
|
39
|
+
iframe,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Wait for the client to be all setup
|
|
43
|
+
await client.waitForSetup;
|
|
44
|
+
|
|
45
|
+
// Wait for the connection to be established
|
|
46
|
+
const waitForConnection = await client.waitForConnection;
|
|
47
|
+
if (!waitForConnection) {
|
|
48
|
+
console.error("Failed to connect to client");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return client;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Prepare the config for the Frak Client
|
|
57
|
+
* @param config - The configuration to use for the Frak Wallet SDK
|
|
58
|
+
* @returns The prepared configuration with the supported currency
|
|
59
|
+
*/
|
|
60
|
+
function prepareConfig(config: FrakWalletSdkConfig): FrakWalletSdkConfig {
|
|
61
|
+
// Get the supported currency (e.g. "eur")
|
|
62
|
+
const supportedCurrency = getSupportedCurrency(config.metadata?.currency);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
...config,
|
|
66
|
+
metadata: {
|
|
67
|
+
...config.metadata,
|
|
68
|
+
currency: supportedCurrency,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Deferred } from "@frak-labs/frame-connector";
|
|
2
|
+
import type { FrakLifecycleEvent } from "../../types";
|
|
3
|
+
import { BACKUP_KEY } from "../../utils/constants";
|
|
4
|
+
import { changeIframeVisibility } from "../../utils/iframeHelper";
|
|
5
|
+
|
|
6
|
+
/** @ignore */
|
|
7
|
+
export type IframeLifecycleManager = {
|
|
8
|
+
isConnected: Promise<boolean>;
|
|
9
|
+
handleEvent: (messageEvent: FrakLifecycleEvent) => Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a new iframe lifecycle handler
|
|
14
|
+
* @ignore
|
|
15
|
+
*/
|
|
16
|
+
export function createIFrameLifecycleManager({
|
|
17
|
+
iframe,
|
|
18
|
+
}: { iframe: HTMLIFrameElement }): IframeLifecycleManager {
|
|
19
|
+
// Create the isConnected listener
|
|
20
|
+
const isConnectedDeferred = new Deferred<boolean>();
|
|
21
|
+
|
|
22
|
+
// Build the handler itself
|
|
23
|
+
const handler = async (messageEvent: FrakLifecycleEvent) => {
|
|
24
|
+
if (!("iframeLifecycle" in messageEvent)) return;
|
|
25
|
+
|
|
26
|
+
const { iframeLifecycle: event, data } = messageEvent;
|
|
27
|
+
|
|
28
|
+
switch (event) {
|
|
29
|
+
// Resolve the isConnected promise
|
|
30
|
+
case "connected":
|
|
31
|
+
isConnectedDeferred.resolve(true);
|
|
32
|
+
break;
|
|
33
|
+
// Perform a frak backup
|
|
34
|
+
case "do-backup":
|
|
35
|
+
if (data.backup) {
|
|
36
|
+
localStorage.setItem(BACKUP_KEY, data.backup);
|
|
37
|
+
} else {
|
|
38
|
+
localStorage.removeItem(BACKUP_KEY);
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
// Remove frak backup
|
|
42
|
+
case "remove-backup":
|
|
43
|
+
localStorage.removeItem(BACKUP_KEY);
|
|
44
|
+
break;
|
|
45
|
+
// Change iframe visibility
|
|
46
|
+
case "show":
|
|
47
|
+
case "hide":
|
|
48
|
+
changeIframeVisibility({
|
|
49
|
+
iframe,
|
|
50
|
+
isVisible: event === "show",
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
// Handshake handling
|
|
54
|
+
case "handshake": {
|
|
55
|
+
iframe.contentWindow?.postMessage(
|
|
56
|
+
{
|
|
57
|
+
clientLifecycle: "handshake-response",
|
|
58
|
+
data: {
|
|
59
|
+
token: data.token,
|
|
60
|
+
currentUrl: window.location.href,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
"*"
|
|
64
|
+
);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
// Redirect handling
|
|
68
|
+
case "redirect": {
|
|
69
|
+
const redirectUrl = new URL(data.baseRedirectUrl);
|
|
70
|
+
|
|
71
|
+
// If we got a u append the current location dynamicly
|
|
72
|
+
if (redirectUrl.searchParams.has("u")) {
|
|
73
|
+
redirectUrl.searchParams.delete("u");
|
|
74
|
+
redirectUrl.searchParams.append("u", window.location.href);
|
|
75
|
+
window.location.href = redirectUrl.toString();
|
|
76
|
+
} else {
|
|
77
|
+
window.location.href = data.baseRedirectUrl;
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
handleEvent: handler,
|
|
86
|
+
isConnected: isConnectedDeferred.promise,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The final keys for each interaction types (e.g. `openArticle`) -> interaction type
|
|
3
|
+
* @inline
|
|
4
|
+
*/
|
|
5
|
+
export type InteractionTypesKey = {
|
|
6
|
+
[K in keyof typeof interactionTypes]: keyof (typeof interactionTypes)[K];
|
|
7
|
+
}[keyof typeof interactionTypes];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The keys for each interaction types (e.g. `press.openArticle`) -> category_type.interaction_type
|
|
11
|
+
* @inline
|
|
12
|
+
*/
|
|
13
|
+
export type FullInteractionTypesKey = {
|
|
14
|
+
[Category in keyof typeof interactionTypes]: `${Category & string}.${keyof (typeof interactionTypes)[Category] & string}`;
|
|
15
|
+
}[keyof typeof interactionTypes];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Each interactions types according to the product types
|
|
19
|
+
*/
|
|
20
|
+
export const interactionTypes = {
|
|
21
|
+
press: {
|
|
22
|
+
openArticle: "0xc0a24ffb",
|
|
23
|
+
readArticle: "0xd5bd0fbe",
|
|
24
|
+
},
|
|
25
|
+
dapp: {
|
|
26
|
+
proofVerifiableStorageUpdate: "0x2ab2aeef",
|
|
27
|
+
callableVerifiableStorageUpdate: "0xa07da986",
|
|
28
|
+
},
|
|
29
|
+
webshop: {
|
|
30
|
+
open: "0xb311798f",
|
|
31
|
+
},
|
|
32
|
+
referral: {
|
|
33
|
+
referred: "0x010cc3b9",
|
|
34
|
+
createLink: "0xb2c0f17c",
|
|
35
|
+
},
|
|
36
|
+
purchase: {
|
|
37
|
+
started: "0xd87e90c3",
|
|
38
|
+
completed: "0x8403aeb4",
|
|
39
|
+
unsafeCompleted: "0x4d5b14e0",
|
|
40
|
+
},
|
|
41
|
+
retail: {
|
|
42
|
+
customerMeeting: "0x74489004",
|
|
43
|
+
},
|
|
44
|
+
} as const;
|