@frak-labs/core-sdk 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/cdn/bundle.js +14 -0
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +3 -3
- package/dist/actions.d.ts +3 -3
- package/dist/actions.js +1 -1
- package/dist/bundle.cjs +1 -1
- package/dist/bundle.d.cts +4 -6
- package/dist/bundle.d.ts +4 -6
- package/dist/bundle.js +1 -1
- package/dist/{index-CRsQWnTs.d.cts → computeLegacyProductId-BkyJ4rEY.d.ts} +197 -10
- package/dist/{index-Ck1hudEi.d.ts → computeLegacyProductId-Raks6FXg.d.cts} +197 -10
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -4
- package/dist/index.d.ts +3 -4
- package/dist/index.js +1 -1
- package/dist/{openSso-D--Airj6.d.cts → openSso-BCJGchIb.d.cts} +135 -131
- package/dist/{openSso-DsKJ4y0j.d.ts → openSso-DG-_9CED.d.ts} +135 -131
- package/dist/setupClient-CQrMDGyZ.js +13 -0
- package/dist/setupClient-Ccv3XxwL.cjs +13 -0
- package/dist/{index-d8xS4ryI.d.ts → siweAuthenticate-BH7Dn7nZ.d.cts} +90 -65
- package/dist/siweAuthenticate-BJHbtty4.js +1 -0
- package/dist/{index-C6FxkWPC.d.cts → siweAuthenticate-Btem4QHs.d.ts} +90 -65
- package/dist/siweAuthenticate-Cwj3HP0m.cjs +1 -0
- package/dist/trackEvent-M2RLTQ2p.js +1 -0
- package/dist/trackEvent-T_R9ER2S.cjs +1 -0
- package/package.json +11 -22
- package/src/actions/displayEmbeddedWallet.ts +1 -0
- package/src/actions/displayModal.test.ts +12 -11
- package/src/actions/displayModal.ts +7 -18
- package/src/actions/ensureIdentity.ts +68 -0
- package/src/actions/{getProductInformation.test.ts → getMerchantInformation.test.ts} +33 -50
- package/src/actions/getMerchantInformation.ts +16 -0
- package/src/actions/index.ts +3 -2
- package/src/actions/openSso.ts +4 -2
- package/src/actions/referral/processReferral.test.ts +42 -151
- package/src/actions/referral/processReferral.ts +18 -42
- package/src/actions/referral/referralInteraction.test.ts +1 -7
- package/src/actions/referral/referralInteraction.ts +1 -6
- package/src/actions/sendInteraction.ts +46 -22
- package/src/actions/trackPurchaseStatus.test.ts +354 -141
- package/src/actions/trackPurchaseStatus.ts +48 -11
- package/src/actions/watchWalletStatus.ts +2 -3
- package/src/actions/wrapper/modalBuilder.test.ts +0 -14
- package/src/actions/wrapper/modalBuilder.ts +3 -12
- package/src/bundle.ts +0 -1
- package/src/clients/createIFrameFrakClient.ts +10 -5
- package/src/clients/transports/iframeLifecycleManager.test.ts +163 -4
- package/src/clients/transports/iframeLifecycleManager.ts +172 -33
- package/src/constants/interactionTypes.ts +12 -41
- package/src/index.ts +24 -16
- package/src/types/config.ts +6 -0
- package/src/types/index.ts +13 -10
- package/src/types/lifecycle/client.ts +24 -1
- package/src/types/lifecycle/iframe.ts +6 -0
- package/src/types/rpc/displayModal.ts +2 -4
- package/src/types/rpc/embedded/index.ts +2 -2
- package/src/types/rpc/interaction.ts +26 -39
- package/src/types/rpc/merchantInformation.ts +77 -0
- package/src/types/rpc/modal/index.ts +0 -4
- package/src/types/rpc/modal/login.ts +5 -1
- package/src/types/rpc/walletStatus.ts +1 -7
- package/src/types/rpc.ts +22 -30
- package/src/types/tracking.ts +60 -0
- package/src/utils/backendUrl.test.ts +83 -0
- package/src/utils/backendUrl.ts +62 -0
- package/src/utils/clientId.test.ts +41 -0
- package/src/utils/clientId.ts +43 -0
- package/src/utils/compression/compress.test.ts +1 -1
- package/src/utils/compression/compress.ts +2 -2
- package/src/utils/compression/decompress.test.ts +8 -4
- package/src/utils/compression/decompress.ts +2 -2
- package/src/utils/{computeProductId.ts → computeLegacyProductId.ts} +2 -2
- package/src/utils/constants.ts +5 -0
- package/src/utils/deepLinkWithFallback.test.ts +243 -0
- package/src/utils/deepLinkWithFallback.ts +103 -0
- package/src/utils/formatAmount.ts +6 -0
- package/src/utils/iframeHelper.test.ts +18 -5
- package/src/utils/iframeHelper.ts +10 -3
- package/src/utils/index.ts +16 -1
- package/src/utils/merchantId.test.ts +653 -0
- package/src/utils/merchantId.ts +143 -0
- package/src/utils/sso.ts +18 -11
- package/src/utils/trackEvent.test.ts +23 -5
- package/src/utils/trackEvent.ts +13 -0
- package/cdn/bundle.iife.js +0 -14
- package/dist/actions-B5j-i1p0.cjs +0 -1
- package/dist/actions-q090Z0oR.js +0 -1
- package/dist/index-7OZ39x1U.d.ts +0 -195
- package/dist/index-zDq-VlKx.d.cts +0 -195
- package/dist/interaction-DMJ3ZfaF.d.cts +0 -45
- package/dist/interaction-KX1h9a7V.d.ts +0 -45
- package/dist/interactions-DnfM3oe0.js +0 -1
- package/dist/interactions-EIXhNLf6.cjs +0 -1
- package/dist/interactions.cjs +0 -1
- package/dist/interactions.d.cts +0 -2
- package/dist/interactions.d.ts +0 -2
- package/dist/interactions.js +0 -1
- package/dist/productTypes-BUkXJKZ7.cjs +0 -1
- package/dist/productTypes-CGb1MmBF.js +0 -1
- package/dist/src-1LQ4eLq5.js +0 -13
- package/dist/src-hW71KjPN.cjs +0 -13
- package/dist/trackEvent-CHnYa85W.js +0 -1
- package/dist/trackEvent-GuQm_1Nm.cjs +0 -1
- package/src/actions/getProductInformation.ts +0 -14
- package/src/actions/openSso.test.ts +0 -407
- package/src/actions/sendInteraction.test.ts +0 -219
- package/src/constants/interactionTypes.test.ts +0 -128
- package/src/constants/productTypes.test.ts +0 -130
- package/src/constants/productTypes.ts +0 -33
- package/src/interactions/index.ts +0 -5
- package/src/interactions/pressEncoder.test.ts +0 -215
- package/src/interactions/pressEncoder.ts +0 -53
- package/src/interactions/purchaseEncoder.test.ts +0 -291
- package/src/interactions/purchaseEncoder.ts +0 -99
- package/src/interactions/referralEncoder.test.ts +0 -170
- package/src/interactions/referralEncoder.ts +0 -47
- package/src/interactions/retailEncoder.test.ts +0 -107
- package/src/interactions/retailEncoder.ts +0 -37
- package/src/interactions/webshopEncoder.test.ts +0 -56
- package/src/interactions/webshopEncoder.ts +0 -30
- package/src/types/rpc/modal/openSession.ts +0 -25
- package/src/types/rpc/productInformation.ts +0 -59
- package/src/utils/computeProductId.test.ts +0 -80
- package/src/utils/sso.test.ts +0 -361
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { FrakRpcError, RpcErrorCodes } from "@frak-labs/frame-connector";
|
|
2
|
-
import { type Address,
|
|
3
|
-
import { ReferralInteractionEncoder } from "../../interactions";
|
|
2
|
+
import { type Address, isAddressEqual } from "viem";
|
|
4
3
|
import type {
|
|
5
4
|
DisplayEmbeddedWalletParamsType,
|
|
6
5
|
FrakClient,
|
|
@@ -8,7 +7,8 @@ import type {
|
|
|
8
7
|
WalletStatusReturnType,
|
|
9
8
|
} from "../../types";
|
|
10
9
|
import { FrakContextManager, trackEvent } from "../../utils";
|
|
11
|
-
import { displayEmbeddedWallet
|
|
10
|
+
import { displayEmbeddedWallet } from "../displayEmbeddedWallet";
|
|
11
|
+
import { sendInteraction } from "../sendInteraction";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* The different states of the referral process
|
|
@@ -19,7 +19,6 @@ type ReferralState =
|
|
|
19
19
|
| "processing"
|
|
20
20
|
| "success"
|
|
21
21
|
| "no-wallet"
|
|
22
|
-
| "no-session"
|
|
23
22
|
| "error"
|
|
24
23
|
| "no-referrer"
|
|
25
24
|
| "self-referral";
|
|
@@ -41,11 +40,9 @@ export type ProcessReferralOptions = {
|
|
|
41
40
|
* 2. Then check if the user is logged in or not
|
|
42
41
|
* 2.1 If not logged in, try a soft login, if it fail, display a modal for the user to login
|
|
43
42
|
* 3. Check if that's not a self-referral (if yes, early exit)
|
|
44
|
-
* 4.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* 6. Update the current url with the right data
|
|
48
|
-
* 7. Return the resulting referral state
|
|
43
|
+
* 4. Track the referral event
|
|
44
|
+
* 5. Update the current url with the right data
|
|
45
|
+
* 6. Return the resulting referral state
|
|
49
46
|
*
|
|
50
47
|
* If any error occurs during the process, the function will catch it and return an error state
|
|
51
48
|
*
|
|
@@ -54,13 +51,10 @@ export type ProcessReferralOptions = {
|
|
|
54
51
|
* @param args.walletStatus - The current user wallet status
|
|
55
52
|
* @param args.frakContext - The current frak context
|
|
56
53
|
* @param args.modalConfig - The modal configuration to display if the user is not logged in
|
|
57
|
-
* @param args.productId - The product id to interact with (if not specified will be recomputed from the current domain)
|
|
58
54
|
* @param args.options - Some options for the referral interaction
|
|
59
55
|
* @returns A promise with the resulting referral state
|
|
60
56
|
*
|
|
61
57
|
* @see {@link displayModal} for more details about the displayed modal
|
|
62
|
-
* @see {@link sendInteraction} for more details on the interaction submission part
|
|
63
|
-
* @see {@link ReferralInteractionEncoder} for more details about the referred interaction
|
|
64
58
|
* @see {@link @frak-labs/core-sdk!ModalStepTypes} for more details on each modal steps types
|
|
65
59
|
*/
|
|
66
60
|
export async function processReferral(
|
|
@@ -69,13 +63,11 @@ export async function processReferral(
|
|
|
69
63
|
walletStatus,
|
|
70
64
|
frakContext,
|
|
71
65
|
modalConfig,
|
|
72
|
-
productId,
|
|
73
66
|
options,
|
|
74
67
|
}: {
|
|
75
68
|
walletStatus?: WalletStatusReturnType;
|
|
76
69
|
frakContext?: Partial<FrakContext> | null;
|
|
77
70
|
modalConfig?: DisplayEmbeddedWalletParamsType;
|
|
78
|
-
productId?: Hex;
|
|
79
71
|
options?: ProcessReferralOptions;
|
|
80
72
|
}
|
|
81
73
|
) {
|
|
@@ -92,6 +84,13 @@ export async function processReferral(
|
|
|
92
84
|
},
|
|
93
85
|
});
|
|
94
86
|
|
|
87
|
+
sendInteraction(client, {
|
|
88
|
+
type: "arrival",
|
|
89
|
+
referrerWallet: frakContext.r,
|
|
90
|
+
landingUrl:
|
|
91
|
+
typeof window !== "undefined" ? window.location.href : undefined,
|
|
92
|
+
});
|
|
93
|
+
|
|
95
94
|
// Helper to fetch a fresh wallet status
|
|
96
95
|
let walletRequest = false;
|
|
97
96
|
async function getFreshWalletStatus() {
|
|
@@ -112,20 +111,11 @@ export async function processReferral(
|
|
|
112
111
|
});
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
// Helper function to push the interaction
|
|
116
|
-
async function pushReferralInteraction(referrer: Address) {
|
|
117
|
-
const interaction = ReferralInteractionEncoder.referred({
|
|
118
|
-
referrer,
|
|
119
|
-
});
|
|
120
|
-
await sendInteraction(client, { productId, interaction });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
114
|
try {
|
|
124
115
|
// Do the core processing logic
|
|
125
116
|
const { status, currentWallet } = await processReferralLogic({
|
|
126
117
|
initialWalletStatus: walletStatus,
|
|
127
118
|
getFreshWalletStatus,
|
|
128
|
-
pushReferralInteraction,
|
|
129
119
|
// We can enforce this type cause of the condition at the start
|
|
130
120
|
frakContext: frakContext as Pick<FrakContext, "r">,
|
|
131
121
|
});
|
|
@@ -176,42 +166,30 @@ export async function processReferral(
|
|
|
176
166
|
}
|
|
177
167
|
|
|
178
168
|
/**
|
|
179
|
-
*
|
|
180
|
-
* -> And automatically set the referral context in the url
|
|
181
|
-
* @param walletStatus
|
|
182
|
-
* @param frakContext
|
|
169
|
+
* Process referral logic - ensure user is logged in and check for self-referral
|
|
183
170
|
*/
|
|
184
171
|
async function processReferralLogic({
|
|
185
172
|
initialWalletStatus,
|
|
186
173
|
getFreshWalletStatus,
|
|
187
|
-
pushReferralInteraction,
|
|
188
174
|
frakContext,
|
|
189
175
|
}: {
|
|
190
176
|
initialWalletStatus?: WalletStatusReturnType;
|
|
191
177
|
getFreshWalletStatus: () => Promise<Address | undefined>;
|
|
192
|
-
pushReferralInteraction: (referrer: Address) => Promise<void>;
|
|
193
178
|
frakContext: Pick<FrakContext, "r">;
|
|
194
179
|
}) {
|
|
195
180
|
// Get the current wallet, without auto displaying the modal
|
|
196
181
|
let currentWallet = initialWalletStatus?.wallet;
|
|
197
182
|
|
|
198
|
-
// If we don't have a current wallet, display the modal
|
|
183
|
+
// If we don't have a current wallet, display the modal to log in
|
|
199
184
|
if (!currentWallet) {
|
|
200
|
-
// Track the event
|
|
201
185
|
currentWallet = await getFreshWalletStatus();
|
|
202
186
|
}
|
|
203
187
|
|
|
188
|
+
// Check for self-referral
|
|
204
189
|
if (currentWallet && isAddressEqual(frakContext.r, currentWallet)) {
|
|
205
190
|
return { status: "self-referral", currentWallet } as const;
|
|
206
191
|
}
|
|
207
192
|
|
|
208
|
-
// If the current wallet doesn't have an interaction session, display the modal
|
|
209
|
-
if (!initialWalletStatus?.interactionSession) {
|
|
210
|
-
currentWallet = await getFreshWalletStatus();
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Push the referred interaction
|
|
214
|
-
await pushReferralInteraction(frakContext.r);
|
|
215
193
|
return { status: "success", currentWallet } as const;
|
|
216
194
|
}
|
|
217
195
|
|
|
@@ -228,8 +206,8 @@ async function ensureWalletConnected(
|
|
|
228
206
|
walletStatus?: WalletStatusReturnType;
|
|
229
207
|
}
|
|
230
208
|
) {
|
|
231
|
-
// If wallet not connected,
|
|
232
|
-
if (
|
|
209
|
+
// If wallet not connected, display modal
|
|
210
|
+
if (walletStatus?.key !== "connected") {
|
|
233
211
|
const result = await displayEmbeddedWallet(client, modalConfig ?? {});
|
|
234
212
|
return result?.wallet ?? undefined;
|
|
235
213
|
}
|
|
@@ -246,8 +224,6 @@ function mapErrorToState(error: unknown): ReferralState {
|
|
|
246
224
|
switch (error.code) {
|
|
247
225
|
case RpcErrorCodes.walletNotConnected:
|
|
248
226
|
return "no-wallet";
|
|
249
|
-
case RpcErrorCodes.serverErrorForInteractionDelegation:
|
|
250
|
-
return "no-session";
|
|
251
227
|
default:
|
|
252
228
|
return "error";
|
|
253
229
|
}
|
|
@@ -21,9 +21,6 @@ describe("referralInteraction", () => {
|
|
|
21
21
|
request: vi.fn(),
|
|
22
22
|
} as any;
|
|
23
23
|
|
|
24
|
-
const mockProductId =
|
|
25
|
-
"0x0000000000000000000000000000000000000000000000000000000000000002" as Hex;
|
|
26
|
-
|
|
27
24
|
beforeEach(() => {
|
|
28
25
|
vi.clearAllMocks();
|
|
29
26
|
Object.defineProperty(global, "window", {
|
|
@@ -55,8 +52,8 @@ describe("referralInteraction", () => {
|
|
|
55
52
|
|
|
56
53
|
vi.mocked(FrakContextManager.parse).mockReturnValue({} as any);
|
|
57
54
|
vi.mocked(watchWalletStatus).mockResolvedValue({
|
|
55
|
+
key: "connected",
|
|
58
56
|
wallet: "0x123" as Hex,
|
|
59
|
-
interactionSession: true,
|
|
60
57
|
} as any);
|
|
61
58
|
vi.mocked(processReferral).mockResolvedValue("success");
|
|
62
59
|
|
|
@@ -80,7 +77,6 @@ describe("referralInteraction", () => {
|
|
|
80
77
|
vi.mocked(processReferral).mockResolvedValue("success");
|
|
81
78
|
|
|
82
79
|
await referralInteraction(mockClient, {
|
|
83
|
-
productId: mockProductId,
|
|
84
80
|
modalConfig: mockModalConfig as any,
|
|
85
81
|
options: mockOptions,
|
|
86
82
|
});
|
|
@@ -89,7 +85,6 @@ describe("referralInteraction", () => {
|
|
|
89
85
|
walletStatus: mockWalletStatus,
|
|
90
86
|
frakContext: mockContext,
|
|
91
87
|
modalConfig: mockModalConfig,
|
|
92
|
-
productId: mockProductId,
|
|
93
88
|
options: mockOptions,
|
|
94
89
|
});
|
|
95
90
|
});
|
|
@@ -145,7 +140,6 @@ describe("referralInteraction", () => {
|
|
|
145
140
|
mockClient,
|
|
146
141
|
expect.objectContaining({
|
|
147
142
|
modalConfig: undefined,
|
|
148
|
-
productId: undefined,
|
|
149
143
|
options: undefined,
|
|
150
144
|
})
|
|
151
145
|
);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Hex } from "viem";
|
|
2
1
|
import type { DisplayEmbeddedWalletParamsType, FrakClient } from "../../types";
|
|
3
2
|
import { FrakContextManager } from "../../utils";
|
|
4
3
|
import { watchWalletStatus } from "../index";
|
|
@@ -8,10 +7,9 @@ import {
|
|
|
8
7
|
} from "./processReferral";
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
|
-
* Function used to
|
|
10
|
+
* Function used to handle referral interactions
|
|
12
11
|
* @param client - The current Frak Client
|
|
13
12
|
* @param args
|
|
14
|
-
* @param args.productId - The product id to interact with (if not specified will be recomputed from the current domain)
|
|
15
13
|
* @param args.modalConfig - The modal configuration to display if the user is not logged in
|
|
16
14
|
* @param args.options - Some options for the referral interaction
|
|
17
15
|
*
|
|
@@ -25,11 +23,9 @@ import {
|
|
|
25
23
|
export async function referralInteraction(
|
|
26
24
|
client: FrakClient,
|
|
27
25
|
{
|
|
28
|
-
productId,
|
|
29
26
|
modalConfig,
|
|
30
27
|
options,
|
|
31
28
|
}: {
|
|
32
|
-
productId?: Hex;
|
|
33
29
|
modalConfig?: DisplayEmbeddedWalletParamsType;
|
|
34
30
|
options?: ProcessReferralOptions;
|
|
35
31
|
} = {}
|
|
@@ -47,7 +43,6 @@ export async function referralInteraction(
|
|
|
47
43
|
walletStatus: currentWalletStatus,
|
|
48
44
|
frakContext,
|
|
49
45
|
modalConfig,
|
|
50
|
-
productId,
|
|
51
46
|
options,
|
|
52
47
|
});
|
|
53
48
|
} catch (error) {
|
|
@@ -1,32 +1,56 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
SendInteractionReturnType,
|
|
5
|
-
} from "../types";
|
|
6
|
-
import { computeProductId } from "../utils/computeProductId";
|
|
1
|
+
import type { FrakClient } from "../types";
|
|
2
|
+
import type { SendInteractionParamsType } from "../types/rpc/interaction";
|
|
3
|
+
import { getClientId } from "../utils/clientId";
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* Send an interaction to the backend via the listener RPC.
|
|
7
|
+
* Fire-and-forget: errors are caught and logged, not thrown.
|
|
8
|
+
*
|
|
9
|
+
* @param client - The Frak client instance
|
|
10
|
+
* @param params - The interaction parameters
|
|
11
|
+
*
|
|
12
|
+
* @description Sends a user interaction event through the wallet iframe RPC. Supports three interaction types: arrival tracking, sharing events, and custom interactions.
|
|
12
13
|
*
|
|
13
14
|
* @example
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* Track a user arrival with referral attribution:
|
|
16
|
+
* ```ts
|
|
17
|
+
* await sendInteraction(client, {
|
|
18
|
+
* type: "arrival",
|
|
19
|
+
* referrerWallet: "0x1234...abcd",
|
|
20
|
+
* landingUrl: window.location.href,
|
|
21
|
+
* utmSource: "twitter",
|
|
22
|
+
* utmMedium: "social",
|
|
23
|
+
* utmCampaign: "launch-2026",
|
|
16
24
|
* });
|
|
17
|
-
*
|
|
18
|
-
*
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* Track a sharing event:
|
|
29
|
+
* ```ts
|
|
30
|
+
* await sendInteraction(client, { type: "sharing" });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* Send a custom interaction:
|
|
35
|
+
* ```ts
|
|
36
|
+
* await sendInteraction(client, {
|
|
37
|
+
* type: "custom",
|
|
38
|
+
* customType: "newsletter_signup",
|
|
39
|
+
* data: { email: "user@example.com" },
|
|
19
40
|
* });
|
|
20
|
-
*
|
|
41
|
+
* ```
|
|
21
42
|
*/
|
|
22
43
|
export async function sendInteraction(
|
|
23
44
|
client: FrakClient,
|
|
24
|
-
|
|
25
|
-
): Promise<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
45
|
+
params: SendInteractionParamsType
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
try {
|
|
48
|
+
await client.request({
|
|
49
|
+
method: "frak_sendInteraction",
|
|
50
|
+
params: [params, { clientId: getClientId() }],
|
|
51
|
+
});
|
|
52
|
+
} catch {
|
|
53
|
+
// Silent failure - fire-and-forget
|
|
54
|
+
console.warn("[Frak SDK] Failed to send interaction:", params.type);
|
|
55
|
+
}
|
|
32
56
|
}
|