@frak-labs/components 1.0.2 → 1.0.3
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/Banner.BTj-CQM6.js +162 -0
- package/cdn/ButtonShare.FHUOd26e.js +1 -0
- package/cdn/{ButtonWallet.Cwz9qFhE.js → ButtonWallet.CN2iHSTB.js} +1 -1
- package/cdn/{OpenInAppButton.Hq9EjwJE.js → OpenInAppButton.C1Yipwka.js} +1 -1
- package/cdn/PostPurchase.u0s94KFf.js +52 -0
- package/cdn/components.js +1 -1
- package/cdn/formatReward.C7mU9_cV.js +1 -0
- package/cdn/loader.js +1 -1
- package/cdn/sharingPage.CvUkxEML.js +1 -0
- package/cdn/{sprinkles.css.ts.vanilla.Ct795MMK.js → sprinkles.css.ts.vanilla.06k5OzG1.js} +1 -1
- package/cdn/useGlobalComponents.UJmjUUxk.js +1 -0
- package/cdn/{useLightDomStyles.DqYouFn3.js → useLightDomStyles.Gt7YUMDl.js} +1 -1
- package/cdn/{usePlacement.Di6eZ4ty.js → usePlacement.BJ7qe-pw.js} +1 -1
- package/cdn/{useReward.DWyyva4u.js → useReward.QsQc2c1D.js} +1 -1
- package/dist/{GiftIcon-4sr9xXyq.js → GiftIcon-c28NnqJ7.js} +1 -0
- package/dist/banner.js +2 -2
- package/dist/buttonShare.d.ts +9 -4
- package/dist/buttonShare.js +13 -177
- package/dist/openInApp.js +1 -1
- package/dist/postPurchase.d.ts +17 -1
- package/dist/postPurchase.js +96 -16
- package/dist/sharingPage-DFvQbviS.js +15 -0
- package/dist/useLightDomStyles-gbuSWvRx.js +89 -0
- package/package.json +1 -1
- package/cdn/Banner.1iUbfe7Z.js +0 -162
- package/cdn/ButtonShare.APhrT3sb.js +0 -1
- package/cdn/PostPurchase.gHQmp1c4.js +0 -52
- package/cdn/formatReward.BaR9pE50.js +0 -1
- package/cdn/useGlobalComponents.CLH7id-Y.js +0 -1
- package/cdn/useShareModal.pszXJ-rf.js +0 -1
- package/dist/useLightDomStyles-C3lcOwY2.js +0 -41
- package/dist/useShareModal-BEVkLrBP.js +0 -54
package/dist/buttonShare.d.ts
CHANGED
|
@@ -32,10 +32,15 @@ type ButtonShareProps = {
|
|
|
32
32
|
*/
|
|
33
33
|
targetInteraction?: InteractionTypeKey;
|
|
34
34
|
/**
|
|
35
|
-
* Which UI to open on click
|
|
36
|
-
*
|
|
35
|
+
* Which UI to open on click.
|
|
36
|
+
*
|
|
37
|
+
* Legacy values (e.g. `"share-modal"`) are accepted at runtime and
|
|
38
|
+
* gracefully route to the full-page sharing UI — the modal-flow
|
|
39
|
+
* share path was retired in favour of `displaySharingPage`.
|
|
40
|
+
*
|
|
41
|
+
* @defaultValue `"sharing-page"`
|
|
37
42
|
*/
|
|
38
|
-
clickAction?: "embedded-wallet" | "
|
|
43
|
+
clickAction?: "embedded-wallet" | "sharing-page";
|
|
39
44
|
/**
|
|
40
45
|
* When set, renders the button in preview mode (e.g. Shopify/WP editor).
|
|
41
46
|
* Skips the client-ready gating so the button is always enabled visually,
|
|
@@ -84,7 +89,7 @@ type ButtonShareProps = {
|
|
|
84
89
|
* <frak-button-share use-reward text="Share and earn up to {REWARD}!" no-reward-text="Share and earn!" target-interaction="custom.customerMeeting"></frak-button-share>
|
|
85
90
|
* ```
|
|
86
91
|
*
|
|
87
|
-
* @see {@link @frak-labs/core-sdk!actions.
|
|
92
|
+
* @see {@link @frak-labs/core-sdk!actions.displaySharingPage | `displaySharingPage()`} for more info about the sharing-page flow
|
|
88
93
|
* @see {@link @frak-labs/core-sdk!actions.getMerchantInformation | `getMerchantInformation()`} for more info about the estimated reward fetching
|
|
89
94
|
*/
|
|
90
95
|
declare function ButtonShare({
|
package/dist/buttonShare.js
CHANGED
|
@@ -1,176 +1,12 @@
|
|
|
1
1
|
import { a as registerWebComponent, i as useClientReady, s as openEmbeddedWallet, t as usePlacement } from "./usePlacement-V7NrKoub.js";
|
|
2
2
|
import { t as useGlobalComponents } from "./useGlobalComponents-Cmfszr7v.js";
|
|
3
|
-
import { t as useLightDomStyles } from "./useLightDomStyles-
|
|
3
|
+
import { t as useLightDomStyles } from "./useLightDomStyles-gbuSWvRx.js";
|
|
4
4
|
import { t as applyRewardPlaceholder } from "./formatReward-Bub6Z6eY.js";
|
|
5
5
|
import { t as useReward } from "./useReward-DU3_yP8Q.js";
|
|
6
|
-
import { t as
|
|
6
|
+
import { t as openSharingPage } from "./sharingPage-DFvQbviS.js";
|
|
7
7
|
import { trackEvent } from "@frak-labs/core-sdk";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
|
|
11
|
-
//#region src/utils/sharingPage.ts
|
|
12
|
-
async function openSharingPage(targetInteraction, placement) {
|
|
13
|
-
if (!window.FrakSetup?.client) {
|
|
14
|
-
console.error("Frak client not found");
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
await displaySharingPage(window.FrakSetup.client, { metadata: { ...targetInteraction && { targetInteraction } } }, placement);
|
|
18
|
-
}
|
|
19
|
-
//#endregion
|
|
20
|
-
//#region src/hooks/useCopyToClipboard.ts
|
|
21
|
-
function useCopyToClipboard(options = {}) {
|
|
22
|
-
const { successDuration = 2e3 } = options;
|
|
23
|
-
const [copied, setCopied] = useState(false);
|
|
24
|
-
return {
|
|
25
|
-
copy: useCallback(async (text) => {
|
|
26
|
-
try {
|
|
27
|
-
if (navigator.clipboard && window.isSecureContext) {
|
|
28
|
-
await navigator.clipboard.writeText(text);
|
|
29
|
-
setCopied(true);
|
|
30
|
-
} else {
|
|
31
|
-
const textArea = document.createElement("textarea");
|
|
32
|
-
textArea.value = text;
|
|
33
|
-
textArea.style.position = "fixed";
|
|
34
|
-
textArea.style.opacity = "0";
|
|
35
|
-
document.body.appendChild(textArea);
|
|
36
|
-
textArea.focus();
|
|
37
|
-
textArea.select();
|
|
38
|
-
try {
|
|
39
|
-
document.execCommand("copy");
|
|
40
|
-
setCopied(true);
|
|
41
|
-
} catch (err) {
|
|
42
|
-
console.error("Failed to copy text:", err);
|
|
43
|
-
return false;
|
|
44
|
-
} finally {
|
|
45
|
-
textArea.remove();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
setTimeout(() => {
|
|
49
|
-
setCopied(false);
|
|
50
|
-
}, successDuration);
|
|
51
|
-
return true;
|
|
52
|
-
} catch (err) {
|
|
53
|
-
console.error("Failed to copy text:", err);
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}, [successDuration]),
|
|
57
|
-
copied
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
//#endregion
|
|
61
|
-
//#region src/components/ButtonShare/components/ErrorMessage.tsx
|
|
62
|
-
const styles = {
|
|
63
|
-
errorContainer: {
|
|
64
|
-
marginTop: "16px",
|
|
65
|
-
padding: "16px",
|
|
66
|
-
backgroundColor: "#FEE2E2",
|
|
67
|
-
border: "1px solid #FCA5A5",
|
|
68
|
-
borderRadius: "4px",
|
|
69
|
-
color: "#991B1B"
|
|
70
|
-
},
|
|
71
|
-
header: {
|
|
72
|
-
display: "flex",
|
|
73
|
-
alignItems: "center",
|
|
74
|
-
gap: "8px",
|
|
75
|
-
marginBottom: "12px"
|
|
76
|
-
},
|
|
77
|
-
title: {
|
|
78
|
-
margin: 0,
|
|
79
|
-
fontSize: "16px",
|
|
80
|
-
fontWeight: 500
|
|
81
|
-
},
|
|
82
|
-
message: {
|
|
83
|
-
fontSize: "14px",
|
|
84
|
-
lineHeight: "1.5",
|
|
85
|
-
margin: "0 0 12px 0"
|
|
86
|
-
},
|
|
87
|
-
link: {
|
|
88
|
-
color: "#991B1B",
|
|
89
|
-
textDecoration: "underline",
|
|
90
|
-
textUnderlineOffset: "2px"
|
|
91
|
-
},
|
|
92
|
-
copyButton: {
|
|
93
|
-
display: "inline-flex",
|
|
94
|
-
alignItems: "center",
|
|
95
|
-
gap: "8px",
|
|
96
|
-
marginBottom: "10px",
|
|
97
|
-
padding: "8px 12px",
|
|
98
|
-
backgroundColor: "white",
|
|
99
|
-
border: "1px solid #D1D5DB",
|
|
100
|
-
borderRadius: "4px",
|
|
101
|
-
color: "black",
|
|
102
|
-
fontSize: "14px",
|
|
103
|
-
fontWeight: 500
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
/**
|
|
107
|
-
* Renders a toggleable debug information section
|
|
108
|
-
* @param {Object} props - Component props
|
|
109
|
-
* @param {string} [props.debugInfo] - Debug information to display in textarea
|
|
110
|
-
*/
|
|
111
|
-
function ToggleMessage({ debugInfo }) {
|
|
112
|
-
const [showInfo, setShowInfo] = useState(false);
|
|
113
|
-
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("button", {
|
|
114
|
-
type: "button",
|
|
115
|
-
style: styles.copyButton,
|
|
116
|
-
onClick: () => setShowInfo(!showInfo),
|
|
117
|
-
children: "Ouvrir les informations"
|
|
118
|
-
}), showInfo && /* @__PURE__ */ jsx("textarea", {
|
|
119
|
-
style: {
|
|
120
|
-
display: "block",
|
|
121
|
-
width: "100%",
|
|
122
|
-
height: "200px",
|
|
123
|
-
fontSize: "12px"
|
|
124
|
-
},
|
|
125
|
-
children: debugInfo
|
|
126
|
-
})] });
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Displays an error message with debug information and copy functionality
|
|
130
|
-
* @param {Object} props - Component props
|
|
131
|
-
* @param {string} [props.debugInfo] - Debug information that can be copied or displayed
|
|
132
|
-
*/
|
|
133
|
-
function ErrorMessage({ debugInfo }) {
|
|
134
|
-
const { copied, copy } = useCopyToClipboard();
|
|
135
|
-
const handleCopy = () => {
|
|
136
|
-
copy(debugInfo ?? "");
|
|
137
|
-
};
|
|
138
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
139
|
-
style: styles.errorContainer,
|
|
140
|
-
children: [
|
|
141
|
-
/* @__PURE__ */ jsx("div", {
|
|
142
|
-
style: styles.header,
|
|
143
|
-
children: /* @__PURE__ */ jsx("h3", {
|
|
144
|
-
style: styles.title,
|
|
145
|
-
children: "Oups ! Nous avons rencontré un petit problème"
|
|
146
|
-
})
|
|
147
|
-
}),
|
|
148
|
-
/* @__PURE__ */ jsxs("p", {
|
|
149
|
-
style: styles.message,
|
|
150
|
-
children: [
|
|
151
|
-
"Impossible d'ouvrir le menu de partage pour le moment. Si le problème persiste, copiez les informations ci-dessous et collez-les dans votre mail à",
|
|
152
|
-
" ",
|
|
153
|
-
/* @__PURE__ */ jsx("a", {
|
|
154
|
-
href: "mailto:help@frak-labs.com?subject=Debug",
|
|
155
|
-
style: styles.link,
|
|
156
|
-
children: "help@frak-labs.com"
|
|
157
|
-
}),
|
|
158
|
-
" ",
|
|
159
|
-
/* @__PURE__ */ jsx("br", {}),
|
|
160
|
-
"Merci pour votre retour, nous traitons votre demande dans les plus brefs délais."
|
|
161
|
-
]
|
|
162
|
-
}),
|
|
163
|
-
/* @__PURE__ */ jsx("button", {
|
|
164
|
-
type: "button",
|
|
165
|
-
onClick: handleCopy,
|
|
166
|
-
style: styles.copyButton,
|
|
167
|
-
children: copied ? "Informations copiées !" : "Copier les informations de débogage"
|
|
168
|
-
}),
|
|
169
|
-
/* @__PURE__ */ jsx(ToggleMessage, { debugInfo })
|
|
170
|
-
]
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
//#endregion
|
|
8
|
+
import { useCallback, useMemo } from "preact/hooks";
|
|
9
|
+
import { jsx } from "preact/jsx-runtime";
|
|
174
10
|
//#region src/components/ButtonShare/ButtonShare.tsx
|
|
175
11
|
/**
|
|
176
12
|
* Button to share the current page
|
|
@@ -210,7 +46,7 @@ function ErrorMessage({ debugInfo }) {
|
|
|
210
46
|
* <frak-button-share use-reward text="Share and earn up to {REWARD}!" no-reward-text="Share and earn!" target-interaction="custom.customerMeeting"></frak-button-share>
|
|
211
47
|
* ```
|
|
212
48
|
*
|
|
213
|
-
* @see {@link @frak-labs/core-sdk!actions.
|
|
49
|
+
* @see {@link @frak-labs/core-sdk!actions.displaySharingPage | `displaySharingPage()`} for more info about the sharing-page flow
|
|
214
50
|
* @see {@link @frak-labs/core-sdk!actions.getMerchantInformation | `getMerchantInformation()`} for more info about the estimated reward fetching
|
|
215
51
|
*/
|
|
216
52
|
function ButtonShare({ placement: placementId, text = "Share and earn!", classname = "", useReward: rawUseReward, noRewardText, targetInteraction, clickAction: rawClickAction, preview }) {
|
|
@@ -226,7 +62,6 @@ function ButtonShare({ placement: placementId, text = "Share and earn!", classna
|
|
|
226
62
|
const resolvedClickAction = useMemo(() => componentConfig?.clickAction ?? rawClickAction ?? "sharing-page", [componentConfig?.clickAction, rawClickAction]);
|
|
227
63
|
const { shouldRender, isHidden, isClientReady } = useClientReady();
|
|
228
64
|
const { reward } = useReward(shouldUseReward && isClientReady, resolvedTargetInteraction);
|
|
229
|
-
const { handleShare, isError, debugInfo } = useShareModal(resolvedTargetInteraction, placementId);
|
|
230
65
|
const btnText = useMemo(() => {
|
|
231
66
|
if (!shouldUseReward) return resolvedText;
|
|
232
67
|
if (!reward) return resolvedNoRewardText ?? applyRewardPlaceholder(resolvedText, void 0);
|
|
@@ -237,7 +72,7 @@ function ButtonShare({ placement: placementId, text = "Share and earn!", classna
|
|
|
237
72
|
resolvedNoRewardText,
|
|
238
73
|
reward
|
|
239
74
|
]);
|
|
240
|
-
const onClick = useCallback(
|
|
75
|
+
const onClick = useCallback(() => {
|
|
241
76
|
if (isPreview) return;
|
|
242
77
|
trackEvent(window.FrakSetup.client, "share_button_clicked", {
|
|
243
78
|
placement: placementId,
|
|
@@ -245,13 +80,14 @@ function ButtonShare({ placement: placementId, text = "Share and earn!", classna
|
|
|
245
80
|
has_reward: Boolean(reward),
|
|
246
81
|
click_action: resolvedClickAction
|
|
247
82
|
});
|
|
248
|
-
if (resolvedClickAction === "embedded-wallet")
|
|
249
|
-
|
|
250
|
-
|
|
83
|
+
if (resolvedClickAction === "embedded-wallet") {
|
|
84
|
+
openEmbeddedWallet(resolvedTargetInteraction, placementId);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
openSharingPage(resolvedTargetInteraction, placementId);
|
|
251
88
|
}, [
|
|
252
89
|
isPreview,
|
|
253
90
|
resolvedClickAction,
|
|
254
|
-
handleShare,
|
|
255
91
|
resolvedTargetInteraction,
|
|
256
92
|
placementId,
|
|
257
93
|
reward
|
|
@@ -262,13 +98,13 @@ function ButtonShare({ placement: placementId, text = "Share and earn!", classna
|
|
|
262
98
|
"button__fadeIn",
|
|
263
99
|
classname
|
|
264
100
|
].filter(Boolean).join(" ");
|
|
265
|
-
return /* @__PURE__ */
|
|
101
|
+
return /* @__PURE__ */ jsx("button", {
|
|
266
102
|
type: "button",
|
|
267
103
|
disabled: !isPreview && !isClientReady,
|
|
268
104
|
class: buttonClass,
|
|
269
105
|
onClick,
|
|
270
106
|
children: btnText
|
|
271
|
-
})
|
|
107
|
+
});
|
|
272
108
|
}
|
|
273
109
|
//#endregion
|
|
274
110
|
//#region src/components/ButtonShare/index.ts
|
package/dist/openInApp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as registerWebComponent, i as useClientReady, t as usePlacement } from "./usePlacement-V7NrKoub.js";
|
|
2
|
-
import { t as useLightDomStyles } from "./useLightDomStyles-
|
|
2
|
+
import { t as useLightDomStyles } from "./useLightDomStyles-gbuSWvRx.js";
|
|
3
3
|
import { DEEP_LINK_SCHEME, trackEvent, triggerDeepLinkWithFallback } from "@frak-labs/core-sdk";
|
|
4
4
|
import { useMemo } from "preact/hooks";
|
|
5
5
|
import { jsx } from "preact/jsx-runtime";
|
package/dist/postPurchase.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SharingPageProduct } from "@frak-labs/core-sdk";
|
|
1
2
|
import * as _$preact from "preact";
|
|
2
3
|
|
|
3
4
|
//#region src/components/PostPurchase/types.d.ts
|
|
@@ -61,6 +62,20 @@ type PostPurchaseProps = {
|
|
|
61
62
|
* Use `{REWARD}` as placeholder for the reward amount.
|
|
62
63
|
*/
|
|
63
64
|
ctaText?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Optional product cards forwarded to the sharing page when the user
|
|
67
|
+
* clicks the CTA. Accepts either a real {@link SharingPageProduct}
|
|
68
|
+
* array (when set imperatively via the JS property,
|
|
69
|
+
* `el.products = [...]`) or a JSON-stringified array (when set as an
|
|
70
|
+
* HTML attribute, `<frak-post-purchase products='[...]'>`). The HTML
|
|
71
|
+
* attribute path is required for server-rendered surfaces — e.g.
|
|
72
|
+
* WooCommerce / Magento plugins — because `preact-custom-element`
|
|
73
|
+
* delivers attribute values as raw strings.
|
|
74
|
+
*
|
|
75
|
+
* Empty arrays / unparseable strings are treated as "no products" so
|
|
76
|
+
* the sharing page renders without the product card section.
|
|
77
|
+
*/
|
|
78
|
+
products?: SharingPageProduct[] | string;
|
|
64
79
|
/**
|
|
65
80
|
* When set, renders the card in preview mode (e.g. Shopify/WP editor).
|
|
66
81
|
* Bypasses the client-ready / RPC gates that normally hide the card
|
|
@@ -118,7 +133,8 @@ declare function PostPurchase({
|
|
|
118
133
|
refereeText: propRefereeText,
|
|
119
134
|
ctaText: propCtaText,
|
|
120
135
|
preview,
|
|
121
|
-
previewVariant
|
|
136
|
+
previewVariant,
|
|
137
|
+
products
|
|
122
138
|
}: PostPurchaseProps): _$preact.JSX.Element | null;
|
|
123
139
|
//#endregion
|
|
124
140
|
//#region src/components/PostPurchase/index.d.ts
|
package/dist/postPurchase.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { a as registerWebComponent, i as useClientReady, t as usePlacement } from "./usePlacement-V7NrKoub.js";
|
|
2
2
|
import { t as useGlobalComponents } from "./useGlobalComponents-Cmfszr7v.js";
|
|
3
|
-
import { t as useLightDomStyles } from "./useLightDomStyles-
|
|
3
|
+
import { t as useLightDomStyles } from "./useLightDomStyles-gbuSWvRx.js";
|
|
4
4
|
import { n as formatEstimatedReward, t as applyRewardPlaceholder } from "./formatReward-Bub6Z6eY.js";
|
|
5
|
-
import { t as
|
|
6
|
-
import { l as createElement, o as cssSource$4, r as LogoFrak, s as cssSource$3, t as GiftIcon } from "./GiftIcon-
|
|
5
|
+
import { t as openSharingPage } from "./sharingPage-DFvQbviS.js";
|
|
6
|
+
import { c as cssSource$5, l as createElement, o as cssSource$4, r as LogoFrak, s as cssSource$3, t as GiftIcon } from "./GiftIcon-c28NnqJ7.js";
|
|
7
7
|
import { trackEvent } from "@frak-labs/core-sdk";
|
|
8
8
|
import { getMerchantInformation, getUserReferralStatus, trackPurchaseStatus } from "@frak-labs/core-sdk/actions";
|
|
9
|
-
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
|
9
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
|
10
10
|
import { jsx, jsxs } from "preact/jsx-runtime";
|
|
11
11
|
import { FrakRpcError, RpcErrorCodes } from "@frak-labs/frame-connector";
|
|
12
12
|
//#region ../../node_modules/.bun/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
@@ -1394,12 +1394,73 @@ const cssSource$1 = `.PostPurchase_card__5fv5lh0 {
|
|
|
1394
1394
|
color: var(--surface-primary__pbq4aka);
|
|
1395
1395
|
}`;
|
|
1396
1396
|
var card = "PostPurchase_card__5fv5lh0";
|
|
1397
|
-
var cta = "PostPurchase_cta__5fv5lh3";
|
|
1397
|
+
var cta = "PostPurchase_cta__5fv5lh3 reset_element_button__1831jhd6 reset_fieldAppearance__1831jhd2 reset_focusRing__1831jhd1";
|
|
1398
1398
|
var frakLogo = "PostPurchase_frakLogo__5fv5lh6";
|
|
1399
1399
|
var giftIcon = "PostPurchase_giftIcon__5fv5lh5";
|
|
1400
1400
|
var icon = "PostPurchase_icon__5fv5lh4";
|
|
1401
1401
|
var message = "PostPurchase_message__5fv5lh2";
|
|
1402
|
-
const cssSource = cssSource$3 + cssSource$4 + cssSource$1;
|
|
1402
|
+
const cssSource = cssSource$5 + cssSource$3 + cssSource$4 + cssSource$1;
|
|
1403
|
+
//#endregion
|
|
1404
|
+
//#region src/components/PostPurchase/products.ts
|
|
1405
|
+
/**
|
|
1406
|
+
* Whether `value` is a syntactically valid URL with an `http(s):` scheme.
|
|
1407
|
+
*
|
|
1408
|
+
* Used to gate `imageUrl` / `link` fields coming from the public `products`
|
|
1409
|
+
* prop — the listener-side sharing-page builder calls `new URL(...)` on the
|
|
1410
|
+
* incoming product link, and a `javascript:` URL would be a XSS sink in any
|
|
1411
|
+
* consumer that binds the value to an `href`.
|
|
1412
|
+
*/
|
|
1413
|
+
function isHttpUrl(value) {
|
|
1414
|
+
try {
|
|
1415
|
+
const parsed = new URL(value);
|
|
1416
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
1417
|
+
} catch {
|
|
1418
|
+
return false;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Coerce a raw `products` prop value into a candidate array suitable for
|
|
1423
|
+
* per-item normalisation, or null when it cannot be reduced to one.
|
|
1424
|
+
*
|
|
1425
|
+
* Surfaces that set the prop via the JS property (`el.products = [...]`)
|
|
1426
|
+
* deliver a real array; surfaces that bind it as an HTML attribute
|
|
1427
|
+
* (WP / Magento server-render) deliver a JSON-stringified array. Anything
|
|
1428
|
+
* else (truthy non-array non-string, JSON parse failure, JSON that decodes
|
|
1429
|
+
* to a non-array) is treated as "no products" so the share still works
|
|
1430
|
+
* without the product card section.
|
|
1431
|
+
*/
|
|
1432
|
+
function coerceProductCandidates(products) {
|
|
1433
|
+
if (!products) return null;
|
|
1434
|
+
if (Array.isArray(products)) return products;
|
|
1435
|
+
if (typeof products !== "string") return null;
|
|
1436
|
+
try {
|
|
1437
|
+
const parsed = JSON.parse(products);
|
|
1438
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
1439
|
+
} catch {
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Normalise one untrusted candidate into a {@link SharingPageProduct}, or
|
|
1445
|
+
* return null when the candidate has no usable title.
|
|
1446
|
+
*
|
|
1447
|
+
* The `products` prop is a public API boundary (merchants can set it
|
|
1448
|
+
* server-side via WP/Magento or imperatively from arbitrary JS). Each entry
|
|
1449
|
+
* is validated structurally so a malformed `link` reaching `new URL(...)`
|
|
1450
|
+
* downstream would not crash the sharing-page builder, and so a
|
|
1451
|
+
* `javascript:` URL cannot slip through as `imageUrl` / `link`.
|
|
1452
|
+
*/
|
|
1453
|
+
function normalizeProductCandidate(candidate) {
|
|
1454
|
+
if (!candidate || typeof candidate !== "object") return null;
|
|
1455
|
+
const item = candidate;
|
|
1456
|
+
const title = typeof item.title === "string" ? item.title.trim() : "";
|
|
1457
|
+
if (title === "") return null;
|
|
1458
|
+
const entry = { title };
|
|
1459
|
+
if (typeof item.imageUrl === "string" && isHttpUrl(item.imageUrl)) entry.imageUrl = item.imageUrl;
|
|
1460
|
+
if (typeof item.link === "string" && isHttpUrl(item.link)) entry.link = item.link;
|
|
1461
|
+
if (typeof item.utmContent === "string" && item.utmContent !== "") entry.utmContent = item.utmContent;
|
|
1462
|
+
return entry;
|
|
1463
|
+
}
|
|
1403
1464
|
//#endregion
|
|
1404
1465
|
//#region src/components/PostPurchase/PostPurchase.tsx
|
|
1405
1466
|
/**
|
|
@@ -1444,7 +1505,7 @@ function resolvePostPurchaseContext(referralStatus, merchantInfo) {
|
|
|
1444
1505
|
* ></frak-post-purchase>
|
|
1445
1506
|
* ```
|
|
1446
1507
|
*/
|
|
1447
|
-
function PostPurchase({ customerId, orderId, token, sharingUrl, merchantId, placement: placementId, classname = "", variant: forcedVariant, badgeText: propBadgeText, referrerText: propReferrerText, refereeText: propRefereeText, ctaText: propCtaText, preview, previewVariant }) {
|
|
1508
|
+
function PostPurchase({ customerId, orderId, token, sharingUrl, merchantId, placement: placementId, classname = "", variant: forcedVariant, badgeText: propBadgeText, referrerText: propReferrerText, refereeText: propRefereeText, ctaText: propCtaText, preview, previewVariant, products }) {
|
|
1448
1509
|
const isPreview = !!preview;
|
|
1449
1510
|
const { shouldRender, isHidden, isClientReady } = useClientReady();
|
|
1450
1511
|
const placement = usePlacement(placementId);
|
|
@@ -1531,7 +1592,32 @@ function PostPurchase({ customerId, orderId, token, sharingUrl, merchantId, plac
|
|
|
1531
1592
|
placementId,
|
|
1532
1593
|
context?.reward
|
|
1533
1594
|
]);
|
|
1534
|
-
const
|
|
1595
|
+
const parsedProducts = useMemo(() => {
|
|
1596
|
+
const candidates = coerceProductCandidates(products);
|
|
1597
|
+
if (!candidates) return void 0;
|
|
1598
|
+
const sanitized = [];
|
|
1599
|
+
for (const candidate of candidates) {
|
|
1600
|
+
const entry = normalizeProductCandidate(candidate);
|
|
1601
|
+
if (entry) sanitized.push(entry);
|
|
1602
|
+
}
|
|
1603
|
+
return sanitized.length > 0 ? sanitized : void 0;
|
|
1604
|
+
}, [products]);
|
|
1605
|
+
const handleClick = useCallback(() => {
|
|
1606
|
+
if (!resolvedVariant) return;
|
|
1607
|
+
trackEvent(window.FrakSetup?.client, "post_purchase_clicked", {
|
|
1608
|
+
placement: placementId,
|
|
1609
|
+
variant: resolvedVariant
|
|
1610
|
+
});
|
|
1611
|
+
openSharingPage(void 0, placementId, {
|
|
1612
|
+
link: resolvedSharingUrl,
|
|
1613
|
+
products: parsedProducts
|
|
1614
|
+
});
|
|
1615
|
+
}, [
|
|
1616
|
+
resolvedVariant,
|
|
1617
|
+
placementId,
|
|
1618
|
+
resolvedSharingUrl,
|
|
1619
|
+
parsedProducts
|
|
1620
|
+
]);
|
|
1535
1621
|
if (!isPreview && (!shouldRender || isHidden)) return null;
|
|
1536
1622
|
if (!isPreview && (!context || !resolvedVariant)) return null;
|
|
1537
1623
|
if (!resolvedVariant) return null;
|
|
@@ -1557,14 +1643,7 @@ function PostPurchase({ customerId, orderId, token, sharingUrl, merchantId, plac
|
|
|
1557
1643
|
type: "button",
|
|
1558
1644
|
className: `${cta} button`,
|
|
1559
1645
|
disabled: !isPreview && !isClientReady,
|
|
1560
|
-
onClick: isPreview ? void 0 :
|
|
1561
|
-
if (!resolvedVariant) return;
|
|
1562
|
-
trackEvent(window.FrakSetup?.client, "post_purchase_clicked", {
|
|
1563
|
-
placement: placementId,
|
|
1564
|
-
variant: resolvedVariant
|
|
1565
|
-
});
|
|
1566
|
-
handleShare();
|
|
1567
|
-
},
|
|
1646
|
+
onClick: isPreview ? void 0 : handleClick,
|
|
1568
1647
|
children: [texts.cta, /* @__PURE__ */ jsx("svg", {
|
|
1569
1648
|
width: "16",
|
|
1570
1649
|
height: "16",
|
|
@@ -1613,6 +1692,7 @@ registerWebComponent(PostPurchase, "frak-post-purchase", [
|
|
|
1613
1692
|
"referrerText",
|
|
1614
1693
|
"refereeText",
|
|
1615
1694
|
"ctaText",
|
|
1695
|
+
"products",
|
|
1616
1696
|
"preview",
|
|
1617
1697
|
"previewVariant"
|
|
1618
1698
|
], { shadow: false });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { displaySharingPage } from "@frak-labs/core-sdk/actions";
|
|
2
|
+
//#region src/utils/sharingPage.ts
|
|
3
|
+
async function openSharingPage(targetInteraction, placement, options) {
|
|
4
|
+
if (!window.FrakSetup?.client) {
|
|
5
|
+
console.error("Frak client not found");
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
await displaySharingPage(window.FrakSetup.client, {
|
|
9
|
+
...options?.link && { link: options.link },
|
|
10
|
+
...options?.products?.length && { products: options.products },
|
|
11
|
+
...targetInteraction && { metadata: { targetInteraction } }
|
|
12
|
+
}, placement);
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { openSharingPage as t };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { r as lightDomBaseCss } from "./usePlacement-V7NrKoub.js";
|
|
2
|
+
import { useEffect } from "preact/hooks";
|
|
3
|
+
//#region src/utils/styleManager.ts
|
|
4
|
+
/**
|
|
5
|
+
* Tracks every base CSS rule (by exact cssText) already injected into <head>.
|
|
6
|
+
*
|
|
7
|
+
* Each Light DOM component (frak-banner, frak-post-purchase, …) ships a
|
|
8
|
+
* `cssSource` string that contains both component-specific rules AND shared
|
|
9
|
+
* design-system rules (reset, sprinkles, theme tokens) pulled in transitively
|
|
10
|
+
* by vanilla-extract. Without dedup, every component re-emits the same reset
|
|
11
|
+
* class definitions in its own <style> tag, and whichever stylesheet is
|
|
12
|
+
* appended LAST wins for those shared selectors — flipping the cascade order
|
|
13
|
+
* non-deterministically across mount orders.
|
|
14
|
+
*
|
|
15
|
+
* Deduplicating rule-by-rule guarantees that shared rules appear exactly once
|
|
16
|
+
* (in the first component's stylesheet) and component-specific rules always
|
|
17
|
+
* come AFTER them in document order, so component styles win deterministically.
|
|
18
|
+
*/
|
|
19
|
+
const injectedRules = /* @__PURE__ */ new Set();
|
|
20
|
+
function ensureStyle(id, css) {
|
|
21
|
+
const existing = document.getElementById(id);
|
|
22
|
+
if (existing) {
|
|
23
|
+
if (existing.textContent !== css) existing.textContent = css;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const style = document.createElement("style");
|
|
27
|
+
style.id = id;
|
|
28
|
+
style.textContent = css;
|
|
29
|
+
document.head.appendChild(style);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parses `css` and returns a new string containing only rules that have not
|
|
33
|
+
* already been injected into <head>. Tracks injected rules in `injectedRules`.
|
|
34
|
+
*
|
|
35
|
+
* Falls back to the raw input if the browser lacks the constructable
|
|
36
|
+
* `CSSStyleSheet` API or parsing fails — preserves correctness over dedup.
|
|
37
|
+
*/
|
|
38
|
+
function dedupeAgainstInjected(css) {
|
|
39
|
+
if (typeof CSSStyleSheet !== "function") return css;
|
|
40
|
+
let sheet;
|
|
41
|
+
try {
|
|
42
|
+
sheet = new CSSStyleSheet();
|
|
43
|
+
sheet.replaceSync(css);
|
|
44
|
+
} catch {
|
|
45
|
+
return css;
|
|
46
|
+
}
|
|
47
|
+
let out = "";
|
|
48
|
+
for (let i = 0; i < sheet.cssRules.length; i++) {
|
|
49
|
+
const ruleText = sheet.cssRules[i].cssText;
|
|
50
|
+
if (injectedRules.has(ruleText)) continue;
|
|
51
|
+
injectedRules.add(ruleText);
|
|
52
|
+
out += `${ruleText}\n`;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
function injectBase(tag, css) {
|
|
57
|
+
const id = `frak-base-${tag}`;
|
|
58
|
+
if (document.getElementById(id)) return;
|
|
59
|
+
const deduped = dedupeAgainstInjected(css);
|
|
60
|
+
if (!deduped) return;
|
|
61
|
+
const style = document.createElement("style");
|
|
62
|
+
style.id = id;
|
|
63
|
+
style.textContent = deduped;
|
|
64
|
+
document.head.appendChild(style);
|
|
65
|
+
}
|
|
66
|
+
function injectPlacement(tag, placementId, scopedCss) {
|
|
67
|
+
ensureStyle(`frak-placement-${tag}-${placementId}`, scopedCss);
|
|
68
|
+
}
|
|
69
|
+
const styleManager = {
|
|
70
|
+
injectBase,
|
|
71
|
+
injectPlacement
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/hooks/useLightDomStyles.ts
|
|
75
|
+
function useLightDomStyles(tag, placementId, placementCss, baseCss) {
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
styleManager.injectBase(tag, baseCss ?? lightDomBaseCss);
|
|
78
|
+
}, [tag]);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!placementId || !placementCss) return;
|
|
81
|
+
styleManager.injectPlacement(tag, placementId, placementCss);
|
|
82
|
+
}, [
|
|
83
|
+
tag,
|
|
84
|
+
placementId,
|
|
85
|
+
placementCss
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { useLightDomStyles as t };
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"url": "https://twitter.com/QNivelais"
|
|
12
12
|
}
|
|
13
13
|
],
|
|
14
|
-
"version": "1.0.
|
|
14
|
+
"version": "1.0.3",
|
|
15
15
|
"description": "Frak Wallet components, helping any person to interact with the Frak wallet.",
|
|
16
16
|
"repository": {
|
|
17
17
|
"url": "https://github.com/frak-id/wallet",
|