@datlv-trustshop/shopify-inapp-components 0.1.9

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.
Files changed (74) hide show
  1. package/README.md +141 -0
  2. package/dist/components/AppList.d.ts +13 -0
  3. package/dist/components/AppList.js +64 -0
  4. package/dist/components/ArticleList.d.ts +20 -0
  5. package/dist/components/ArticleList.js +174 -0
  6. package/dist/components/ArticleSlide.d.ts +14 -0
  7. package/dist/components/ArticleSlide.js +151 -0
  8. package/dist/components/FooterBanner.d.ts +10 -0
  9. package/dist/components/FooterBanner.js +72 -0
  10. package/dist/components/GrowApps.d.ts +13 -0
  11. package/dist/components/GrowApps.js +213 -0
  12. package/dist/components/ImageLoading.d.ts +15 -0
  13. package/dist/components/ImageLoading.js +66 -0
  14. package/dist/components/PartnerList.d.ts +9 -0
  15. package/dist/components/PartnerList.js +102 -0
  16. package/dist/components/PopupBanner.d.ts +12 -0
  17. package/dist/components/PopupBanner.js +100 -0
  18. package/dist/components/TopBanner.d.ts +14 -0
  19. package/dist/components/TopBanner.js +31 -0
  20. package/dist/components/WhatsNew.d.ts +14 -0
  21. package/dist/components/WhatsNew.js +258 -0
  22. package/dist/components/index.d.ts +9 -0
  23. package/dist/components/index.js +9 -0
  24. package/dist/components/inlineStyles.d.ts +110 -0
  25. package/dist/components/inlineStyles.js +114 -0
  26. package/dist/components/styles.d.ts +152 -0
  27. package/dist/components/styles.js +158 -0
  28. package/dist/core/adapter.d.ts +6 -0
  29. package/dist/core/adapter.js +301 -0
  30. package/dist/core/engine.d.ts +33 -0
  31. package/dist/core/engine.js +176 -0
  32. package/dist/core/fetcher.d.ts +4 -0
  33. package/dist/core/fetcher.js +72 -0
  34. package/dist/core/global-manager.d.ts +99 -0
  35. package/dist/core/global-manager.js +315 -0
  36. package/dist/hooks/index.d.ts +6 -0
  37. package/dist/hooks/index.js +6 -0
  38. package/dist/hooks/useApps.d.ts +3 -0
  39. package/dist/hooks/useApps.js +18 -0
  40. package/dist/hooks/useArticles.d.ts +11 -0
  41. package/dist/hooks/useArticles.js +49 -0
  42. package/dist/hooks/useBanner.d.ts +5 -0
  43. package/dist/hooks/useBanner.js +22 -0
  44. package/dist/hooks/useDashboard.d.ts +11 -0
  45. package/dist/hooks/useDashboard.js +13 -0
  46. package/dist/hooks/useGrowApps.d.ts +10 -0
  47. package/dist/hooks/useGrowApps.js +14 -0
  48. package/dist/hooks/useTranslations.d.ts +3 -0
  49. package/dist/hooks/useTranslations.js +9 -0
  50. package/dist/hooks/useWhatsNew.d.ts +11 -0
  51. package/dist/hooks/useWhatsNew.js +34 -0
  52. package/dist/index.d.ts +18 -0
  53. package/dist/index.js +16 -0
  54. package/dist/provider/DashboardProvider.d.ts +36 -0
  55. package/dist/provider/DashboardProvider.js +184 -0
  56. package/dist/translations/default.d.ts +2 -0
  57. package/dist/translations/default.js +27 -0
  58. package/dist/types/app.d.ts +14 -0
  59. package/dist/types/app.js +1 -0
  60. package/dist/types/article.d.ts +14 -0
  61. package/dist/types/article.js +1 -0
  62. package/dist/types/banner.d.ts +22 -0
  63. package/dist/types/banner.js +1 -0
  64. package/dist/types/dashboard.d.ts +42 -0
  65. package/dist/types/dashboard.js +1 -0
  66. package/dist/types/index.d.ts +6 -0
  67. package/dist/types/index.js +6 -0
  68. package/dist/types/partner.d.ts +8 -0
  69. package/dist/types/partner.js +1 -0
  70. package/dist/types/product-update.d.ts +23 -0
  71. package/dist/types/product-update.js +1 -0
  72. package/dist/types/translations.d.ts +28 -0
  73. package/dist/types/translations.js +1 -0
  74. package/package.json +61 -0
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { useFooterBanner } from "../hooks/useBanner";
4
+ export const FooterBanner = ({ className = "", onClose, onAction, closable = true, sticky = true, }) => {
5
+ const banner = useFooterBanner();
6
+ const [isVisible, setIsVisible] = useState(true);
7
+ const handleClose = () => {
8
+ setIsVisible(false);
9
+ onClose?.();
10
+ };
11
+ const handleAction = () => {
12
+ if (banner) {
13
+ if (banner.link) {
14
+ window.open(banner.link, banner.openType === "new_tab" ? "_blank" : "_self");
15
+ }
16
+ onAction?.(banner);
17
+ }
18
+ };
19
+ if (!banner || !isVisible) {
20
+ return null;
21
+ }
22
+ const styles = {
23
+ backgroundColor: banner.style?.backgroundColor || "#333333",
24
+ color: banner.style?.textColor || "#ffffff",
25
+ };
26
+ const buttonStyles = {
27
+ backgroundColor: banner.style?.buttonColor || "#ffffff",
28
+ color: banner.style?.buttonTextColor || "#333333",
29
+ };
30
+ return (_jsxs("div", { className: `footer-banner ${className}`, style: {
31
+ ...styles,
32
+ position: sticky ? "fixed" : "relative",
33
+ bottom: 0,
34
+ left: 0,
35
+ right: 0,
36
+ padding: "16px 20px",
37
+ display: "flex",
38
+ alignItems: "center",
39
+ justifyContent: "space-between",
40
+ zIndex: sticky ? 1000 : "auto",
41
+ boxShadow: "0 -2px 4px rgba(0, 0, 0, 0.1)",
42
+ }, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", flex: 1 }, children: [banner.icon && (_jsx("img", { src: banner.icon, alt: "", style: {
43
+ width: "32px",
44
+ height: "32px",
45
+ marginRight: "16px",
46
+ } })), _jsxs("div", { style: { flex: 1 }, children: [_jsx("div", { style: {
47
+ fontSize: "16px",
48
+ fontWeight: "bold",
49
+ marginBottom: "4px",
50
+ }, children: banner.title }), banner.description && (_jsx("div", { style: { fontSize: "14px", opacity: 0.9 }, children: banner.description }))] }), banner.image && (_jsx("img", { src: banner.image, alt: "", style: {
51
+ maxHeight: "60px",
52
+ marginLeft: "20px",
53
+ borderRadius: "4px",
54
+ } }))] }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: "12px" }, children: [banner.link && (_jsx("button", { onClick: handleAction, style: {
55
+ ...buttonStyles,
56
+ padding: "8px 20px",
57
+ border: "none",
58
+ borderRadius: "4px",
59
+ fontSize: "14px",
60
+ fontWeight: "600",
61
+ cursor: "pointer",
62
+ whiteSpace: "nowrap",
63
+ }, children: banner.linkText || "Learn More" })), closable && (_jsx("button", { onClick: handleClose, style: {
64
+ background: "none",
65
+ border: "none",
66
+ fontSize: "24px",
67
+ cursor: "pointer",
68
+ color: styles.color,
69
+ opacity: 0.7,
70
+ padding: "0 4px",
71
+ }, "aria-label": "Close banner", children: "\u00D7" }))] })] }));
72
+ };
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import { GrowApp } from "../types/dashboard";
3
+ export interface GrowAppsProps {
4
+ className?: string;
5
+ onAppClick?: (app: GrowApp) => void;
6
+ onDismiss?: () => void;
7
+ dismissKey?: string;
8
+ dismissDuration?: number;
9
+ showDismiss?: boolean;
10
+ showNavigation?: boolean;
11
+ maxItems?: number;
12
+ }
13
+ export declare const GrowApps: React.FC<GrowAppsProps>;
@@ -0,0 +1,213 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useRef, useCallback, useEffect } from "react";
3
+ import { Card, Text, BlockStack, Box, InlineStack, Button, Popover, ActionList, } from "@shopify/polaris";
4
+ import { ExternalIcon, MenuHorizontalIcon } from "@shopify/polaris-icons";
5
+ import { useDashboard } from "../hooks/useDashboard";
6
+ import { useTranslation } from "../hooks/useTranslations";
7
+ const DEFAULT_DISMISS_KEY = "ts-dashboard-growapps-dismissed";
8
+ const DEFAULT_DISMISS_DURATION = 7 * 24 * 60 * 60 * 1000;
9
+ export const GrowApps = ({ className = "", onAppClick, onDismiss, dismissKey = DEFAULT_DISMISS_KEY, dismissDuration = DEFAULT_DISMISS_DURATION, showDismiss = true, showNavigation = true, maxItems = 6, }) => {
10
+ const slidesContainerRef = useRef(null);
11
+ const currentPositionRef = useRef(0);
12
+ const { data } = useDashboard();
13
+ const t = useTranslation('growApps');
14
+ const allGrowApps = data?.grow_apps || [];
15
+ const displayLimit = data?.grow_apps_display_limit || maxItems;
16
+ const growApps = allGrowApps.slice(0, displayLimit);
17
+ const [currentIndex, setCurrentIndex] = useState(0);
18
+ const [maxIndex, setMaxIndex] = useState(0);
19
+ const [popoverActive, setPopoverActive] = useState(false);
20
+ const [isVisible, setIsVisible] = useState(true);
21
+ const touchStartX = useRef(null);
22
+ const touchEndX = useRef(null);
23
+ const swiping = useRef(false);
24
+ const togglePopoverActive = () => setPopoverActive((active) => !active);
25
+ const handleDismiss = () => {
26
+ setIsVisible(false);
27
+ setPopoverActive(false);
28
+ const dismissData = {
29
+ timestamp: Date.now(),
30
+ dismissed: true,
31
+ };
32
+ localStorage.setItem(dismissKey, JSON.stringify(dismissData));
33
+ onDismiss?.();
34
+ };
35
+ useEffect(() => {
36
+ const dismissedData = localStorage.getItem(dismissKey);
37
+ if (dismissedData) {
38
+ try {
39
+ const { timestamp } = JSON.parse(dismissedData);
40
+ const now = Date.now();
41
+ if (now - timestamp < dismissDuration) {
42
+ setIsVisible(false);
43
+ }
44
+ else {
45
+ localStorage.removeItem(dismissKey);
46
+ }
47
+ }
48
+ catch (error) {
49
+ localStorage.removeItem(dismissKey);
50
+ }
51
+ }
52
+ }, [dismissKey, dismissDuration]);
53
+ const handleGetAppClick = (app) => {
54
+ const url = app.app_url || app.get_app || "#";
55
+ if (url && url !== "#") {
56
+ window.open(url, "_blank");
57
+ }
58
+ onAppClick?.(app);
59
+ };
60
+ const handleSlide = useCallback((direction) => {
61
+ if (!slidesContainerRef.current) {
62
+ return;
63
+ }
64
+ const slideWrap = slidesContainerRef.current.querySelector(".slides-wrapper");
65
+ if (!slideWrap)
66
+ return;
67
+ const containerWidth = slidesContainerRef.current.offsetWidth;
68
+ const totalWidth = slideWrap.scrollWidth;
69
+ const slideWidth = 282;
70
+ const gap = 20;
71
+ const slideStep = slideWidth + gap;
72
+ const maxSlides = Math.max(0, Math.ceil((totalWidth - containerWidth) / slideStep));
73
+ const newIndex = Math.max(0, Math.min(currentIndex + direction, maxSlides));
74
+ if ((direction < 0 && currentIndex === 0) ||
75
+ (direction > 0 && currentIndex >= maxSlides)) {
76
+ return;
77
+ }
78
+ const newPosition = -newIndex * slideStep;
79
+ slideWrap.style.transform = `translateX(${newPosition}px)`;
80
+ currentPositionRef.current = newPosition;
81
+ setCurrentIndex(newIndex);
82
+ setMaxIndex(maxSlides);
83
+ }, [currentIndex]);
84
+ const handleTouchStart = (e) => {
85
+ touchStartX.current = e.touches[0].clientX;
86
+ swiping.current = true;
87
+ };
88
+ const handleTouchMove = (e) => {
89
+ if (!swiping.current)
90
+ return;
91
+ touchEndX.current = e.touches[0].clientX;
92
+ };
93
+ const handleTouchEnd = () => {
94
+ if (!swiping.current)
95
+ return;
96
+ const swipeThreshold = 50;
97
+ const diff = (touchStartX.current || 0) - (touchEndX.current || 0);
98
+ if (Math.abs(diff) > swipeThreshold) {
99
+ if (diff > 0) {
100
+ handleSlide(1);
101
+ }
102
+ else {
103
+ handleSlide(-1);
104
+ }
105
+ }
106
+ swiping.current = false;
107
+ touchStartX.current = null;
108
+ touchEndX.current = null;
109
+ };
110
+ useEffect(() => {
111
+ const calculateMaxIndex = () => {
112
+ if (!slidesContainerRef.current)
113
+ return;
114
+ const slideWrap = slidesContainerRef.current.querySelector(".slides-wrapper");
115
+ if (!slideWrap)
116
+ return;
117
+ const containerWidth = slidesContainerRef.current.offsetWidth;
118
+ const totalWidth = slideWrap.scrollWidth;
119
+ const slideWidth = 282;
120
+ const gap = 20;
121
+ const slideStep = slideWidth + gap;
122
+ const maxSlides = Math.max(0, Math.ceil((totalWidth - containerWidth) / slideStep));
123
+ setMaxIndex(maxSlides);
124
+ };
125
+ setTimeout(calculateMaxIndex, 100);
126
+ const handleResize = () => calculateMaxIndex();
127
+ window.addEventListener("resize", handleResize);
128
+ return () => {
129
+ window.removeEventListener("resize", handleResize);
130
+ };
131
+ }, [growApps.length]);
132
+ useEffect(() => { }, [growApps]);
133
+ if (!isVisible) {
134
+ return null;
135
+ }
136
+ if (growApps.length === 0) {
137
+ return (_jsx("div", { className: className, style: { width: "100%" }, children: _jsx(Card, { children: _jsx(Box, { children: _jsx(Text, { variant: "bodyMd", tone: "subdued", as: "p", children: t.noData || "No apps available" }) }) }) }));
138
+ }
139
+ const activator = (_jsx(Button, { icon: MenuHorizontalIcon, variant: "tertiary", onClick: togglePopoverActive, ariaExpanded: popoverActive }));
140
+ return (_jsx("div", { className: className, style: { width: "100%" }, children: _jsx(Card, { children: _jsx(Box, { children: _jsxs(BlockStack, { gap: "200", children: [_jsxs(InlineStack, { align: "space-between", blockAlign: "center", wrap: false, gap: "300", children: [_jsxs(BlockStack, { gap: "100", children: [_jsx(Text, { variant: "headingMd", as: "h3", children: t.title || "Grow faster with apps" }), _jsx(Text, { variant: "bodyMd", tone: "subdued", as: "p", children: t.subtitle || "Discover powerful apps to enhance your store, streamline workflows, and boost sales." })] }), _jsxs(InlineStack, { gap: "100", wrap: false, children: [showDismiss && (_jsx(Box, { padding: "150", children: _jsx(Popover, { active: popoverActive, activator: activator, onClose: togglePopoverActive, ariaHaspopup: false, children: _jsx(ActionList, { actionRole: "menuitem", items: [
141
+ {
142
+ content: t.dismiss || "Dismiss",
143
+ onAction: handleDismiss,
144
+ },
145
+ ] }) }) })), showNavigation && growApps.length > 3 && (_jsxs("div", { style: {
146
+ position: "relative",
147
+ width: "fit-content",
148
+ boxSizing: "content-box",
149
+ padding: "4px 6px",
150
+ display: "flex",
151
+ flexDirection: "row",
152
+ flexWrap: "nowrap",
153
+ gap: "4px",
154
+ borderRadius: "8px",
155
+ backgroundColor: "#e3e3e3",
156
+ }, children: [_jsx("button", { onClick: () => handleSlide(-1), disabled: currentIndex === 0, style: {
157
+ display: "flex",
158
+ alignItems: "center",
159
+ justifyContent: "center",
160
+ width: "24px",
161
+ height: "24px",
162
+ padding: 0,
163
+ border: "none",
164
+ borderRadius: "4px",
165
+ backgroundColor: "transparent",
166
+ cursor: currentIndex === 0 ? "not-allowed" : "pointer",
167
+ transition: "background-color 0.2s ease",
168
+ opacity: currentIndex === 0 ? 0.5 : 1,
169
+ }, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: _jsx("path", { d: "M10.5 13L5.5 8l5-5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }) }) }), _jsx("button", { onClick: () => handleSlide(1), disabled: currentIndex >= maxIndex, style: {
170
+ display: "flex",
171
+ alignItems: "center",
172
+ justifyContent: "center",
173
+ width: "24px",
174
+ height: "24px",
175
+ padding: 0,
176
+ border: "none",
177
+ borderRadius: "4px",
178
+ backgroundColor: "transparent",
179
+ cursor: currentIndex >= maxIndex ? "not-allowed" : "pointer",
180
+ transition: "background-color 0.2s ease",
181
+ opacity: currentIndex >= maxIndex ? 0.5 : 1,
182
+ }, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: _jsx("path", { d: "M5.5 13l5-5-5-5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }) }) })] }))] })] }), _jsx("div", { className: "slides-container", style: {
183
+ overflow: "hidden",
184
+ width: "100%",
185
+ }, ref: slidesContainerRef, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: _jsx("div", { className: "slides-wrapper", style: {
186
+ display: "flex",
187
+ gap: "20px",
188
+ transition: "transform 0.3s ease-in-out",
189
+ transform: "translateX(0px)",
190
+ }, children: growApps.map((app, index) => {
191
+ const iconUrl = app.icon_url || app.imageUrl || "";
192
+ const buttonText = app.button_text || app.button_get_app || t.install || "Get app";
193
+ return (_jsx("div", { "data-index": index, style: {
194
+ flexShrink: 0,
195
+ width: "282px",
196
+ transition: "transform 0.2s ease",
197
+ }, children: _jsx(Card, { children: _jsx(Box, { minWidth: "250px", children: _jsxs(BlockStack, { gap: "300", children: [_jsx(InlineStack, { gap: "400", blockAlign: "start", children: iconUrl && (_jsx("img", { src: iconUrl, alt: app.title, width: 60, height: 60, style: {
198
+ borderRadius: "8px",
199
+ flexShrink: 0,
200
+ } })) }), _jsxs(BlockStack, { gap: "200", children: [_jsx(Text, { variant: "headingSm", as: "h4", children: app.title }), _jsx("div", { style: {
201
+ display: "-webkit-box",
202
+ WebkitLineClamp: 3,
203
+ WebkitBoxOrient: "vertical",
204
+ overflow: "hidden",
205
+ textOverflow: "ellipsis",
206
+ fontSize: "13px",
207
+ color: "#616161",
208
+ lineHeight: 1.4,
209
+ }, children: app.content }), _jsx("div", { style: {
210
+ alignSelf: "flex-start",
211
+ }, children: _jsx(Button, { icon: ExternalIcon, variant: "secondary", size: "medium", onClick: () => handleGetAppClick(app), children: buttonText }) })] })] }) }) }) }, app.id || index));
212
+ }) }) })] }) }) }) }));
213
+ };
@@ -0,0 +1,15 @@
1
+ interface ImageLoadingProps {
2
+ src: string;
3
+ alt: string;
4
+ width: number;
5
+ height: number;
6
+ background?: string;
7
+ className?: string;
8
+ onLoad?: () => void;
9
+ onError?: () => void;
10
+ aspectRatio?: number;
11
+ borderRadius?: number;
12
+ overflow?: string;
13
+ }
14
+ declare const ImageLoading: import("react").NamedExoticComponent<ImageLoadingProps>;
15
+ export default ImageLoading;
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { memo, useEffect, useRef } from "react";
3
+ const ImageLoading = memo(({ src, alt, width, height, background = "var(--p-color-bg-fill-tertiary)", className = "", onLoad, onError, aspectRatio, borderRadius = 0, overflow = "hidden", }) => {
4
+ const canvasRef = useRef(null);
5
+ useEffect(() => {
6
+ const canvas = canvasRef.current;
7
+ if (!canvas)
8
+ return;
9
+ const ctx = canvas.getContext("2d");
10
+ if (!ctx)
11
+ return;
12
+ const devicePixelRatio = window.devicePixelRatio || 1;
13
+ canvas.width = width * devicePixelRatio;
14
+ canvas.height = height * devicePixelRatio;
15
+ ctx.scale(devicePixelRatio, devicePixelRatio);
16
+ ctx.fillStyle = "#f3f4f6";
17
+ ctx.fillRect(0, 0, width, height);
18
+ const img = new Image();
19
+ img.onload = () => {
20
+ ctx.clearRect(0, 0, width, height);
21
+ ctx.imageSmoothingEnabled = true;
22
+ ctx.imageSmoothingQuality = "high";
23
+ const imgAspect = img.naturalWidth / img.naturalHeight;
24
+ const canvasAspect = width / height;
25
+ let drawWidth, drawHeight, offsetX, offsetY;
26
+ if (imgAspect > canvasAspect) {
27
+ drawHeight = height;
28
+ drawWidth = height * imgAspect;
29
+ offsetX = (width - drawWidth) / 2;
30
+ offsetY = 0;
31
+ }
32
+ else {
33
+ drawWidth = width;
34
+ drawHeight = width / imgAspect;
35
+ offsetX = 0;
36
+ offsetY = (height - drawHeight) / 2;
37
+ }
38
+ ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
39
+ onLoad?.();
40
+ };
41
+ img.onerror = () => {
42
+ onError?.();
43
+ };
44
+ img.src = src;
45
+ return () => {
46
+ img.onload = null;
47
+ img.onerror = null;
48
+ };
49
+ }, [src, width, height, onLoad, onError]);
50
+ return (_jsx("div", { style: {
51
+ width: "100%",
52
+ aspectRatio: height ? `${width / height}` : `${aspectRatio}`,
53
+ background: background,
54
+ borderRadius: borderRadius + "px",
55
+ overflow: overflow,
56
+ }, className: className, children: _jsx("canvas", { ref: canvasRef, role: "img", "aria-label": alt, style: {
57
+ display: "block",
58
+ width: "100%",
59
+ height: "100%",
60
+ maxWidth: width + "px",
61
+ maxHeight: height + "px",
62
+ aspectRatio: width / height,
63
+ } }) }));
64
+ });
65
+ ImageLoading.displayName = "ImageLoading";
66
+ export default ImageLoading;
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ import { PartnerItem } from "../types";
3
+ export interface PartnerListProps {
4
+ className?: string;
5
+ onPartnerClick?: (partner: PartnerItem) => void;
6
+ onSetupClick?: (partner: PartnerItem) => void;
7
+ showIntegrationStatus?: boolean;
8
+ }
9
+ export declare const PartnerList: React.FC<PartnerListProps>;
@@ -0,0 +1,102 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Badge, BlockStack, Box, Button, Card, InlineStack, Modal, Text, TextContainer, } from "@shopify/polaris";
4
+ import { ExternalIcon } from "@shopify/polaris-icons";
5
+ import { useDashboard } from "../hooks";
6
+ export const PartnerList = ({ className = "", onPartnerClick, onSetupClick, showIntegrationStatus = false, }) => {
7
+ const [modalContent, setModalContent] = useState({
8
+ isOpen: false,
9
+ partner: null,
10
+ type: null,
11
+ });
12
+ const { data: dashboardData } = useDashboard();
13
+ const partnerList = dashboardData?.partner_list || [];
14
+ const handlePartnerAction = (partner) => {
15
+ if (shouldShowModal(partner.key)) {
16
+ setModalContent({
17
+ isOpen: true,
18
+ partner,
19
+ type: getModalType(partner.key),
20
+ });
21
+ }
22
+ else if (partner.app_url) {
23
+ window.open(partner.app_url, "_blank");
24
+ }
25
+ onPartnerClick?.(partner);
26
+ };
27
+ const shouldShowModal = (key) => {
28
+ const modalPartners = [
29
+ "aftership",
30
+ "after_ship",
31
+ "langwill",
32
+ "lang_will",
33
+ "searchanise",
34
+ "tidio",
35
+ ];
36
+ return modalPartners.includes(key.toLowerCase());
37
+ };
38
+ const getModalType = (key) => {
39
+ const setupPartners = ["aftership", "after_ship", "langwill", "lang_will"];
40
+ return setupPartners.includes(key.toLowerCase()) ? "setup" : "info";
41
+ };
42
+ const handleModalClose = () => {
43
+ setModalContent({
44
+ isOpen: false,
45
+ partner: null,
46
+ type: null,
47
+ });
48
+ };
49
+ const handleModalSetup = () => {
50
+ if (modalContent.partner) {
51
+ onSetupClick?.(modalContent.partner);
52
+ if (modalContent.partner.app_url) {
53
+ window.open(modalContent.partner.app_url, "_blank");
54
+ }
55
+ }
56
+ handleModalClose();
57
+ };
58
+ const getIntegrationStatus = (partner) => {
59
+ const activePartners = ["aftership", "after_ship"];
60
+ return activePartners.includes(partner.key.toLowerCase());
61
+ };
62
+ if (partnerList.length === 0) {
63
+ return (_jsx("div", { className: className, style: { width: "100%" }, children: _jsx(Card, { children: _jsx(Box, { padding: "400", children: _jsx(Text, { as: "p", variant: "bodyMd", tone: "subdued", children: "No partner apps available" }) }) }) }));
64
+ }
65
+ return (_jsxs("div", { className: `dashboard-partner-list ${className}`, children: [_jsx(Card, { children: _jsxs(BlockStack, { gap: "400", children: [_jsx(Box, { paddingInlineStart: { xs: "200", md: "0" }, children: _jsx(Text, { variant: "headingMd", as: "h2", children: "Partner Apps & Integrations" }) }), _jsx(BlockStack, { gap: "400", children: partnerList.map((partner, index) => {
66
+ const uniqueKey = `${partner.key}-${index}`;
67
+ const isActive = showIntegrationStatus && getIntegrationStatus(partner);
68
+ const requiresModal = shouldShowModal(partner.key);
69
+ return (_jsx(Card, { children: _jsxs(BlockStack, { gap: "200", children: [_jsxs(InlineStack, { blockAlign: "center", align: "space-between", gap: "100", children: [_jsxs(InlineStack, { blockAlign: "center", gap: "200", children: [partner.icon_url && (_jsx("img", { src: partner.icon_url, alt: partner.title, style: {
70
+ width: "40px",
71
+ height: "40px",
72
+ borderRadius: "8px",
73
+ objectFit: "cover",
74
+ } })), _jsx(Text, { as: "h3", variant: "bodyLg", fontWeight: "semibold", children: partner.title }), showIntegrationStatus && (_jsx(Badge, { tone: isActive ? "success" : "info", children: isActive ? "Active" : "Inactive" }))] }), _jsxs(InlineStack, { gap: "200", children: [_jsx(Button, { icon: requiresModal ? undefined : ExternalIcon, onClick: () => handlePartnerAction(partner), children: partner.button_text || "Install App" }), isActive && (_jsx(Button, { variant: "plain", onClick: () => {
75
+ setModalContent({
76
+ isOpen: true,
77
+ partner,
78
+ type: "setup",
79
+ });
80
+ }, children: "Manage" }))] })] }), _jsx(Text, { as: "p", variant: "bodyMd", children: partner.content })] }) }, uniqueKey));
81
+ }) })] }) }), _jsx(Modal, { open: modalContent.isOpen, onClose: handleModalClose, title: modalContent.type === "setup"
82
+ ? `Set up ${modalContent.partner?.title}`
83
+ : modalContent.partner?.title || "Partner App", primaryAction: modalContent.type === "setup"
84
+ ? {
85
+ content: "Continue Setup",
86
+ onAction: handleModalSetup,
87
+ }
88
+ : {
89
+ content: "Got it",
90
+ onAction: handleModalClose,
91
+ }, secondaryActions: [
92
+ {
93
+ content: "Cancel",
94
+ onAction: handleModalClose,
95
+ },
96
+ ], children: _jsx(Modal.Section, { children: _jsx(TextContainer, { children: _jsxs(BlockStack, { gap: "300", children: [modalContent.partner?.icon_url && (_jsx(Box, { children: _jsx("img", { src: modalContent.partner.icon_url, alt: modalContent.partner.title, style: {
97
+ width: "60px",
98
+ height: "60px",
99
+ borderRadius: "12px",
100
+ objectFit: "cover",
101
+ } }) })), _jsx(Text, { as: "p", variant: "bodyMd", children: modalContent.type === "setup" ? (_jsxs(_Fragment, { children: ["To use ", modalContent.partner?.title, ", you'll need to:", _jsx("br", {}), _jsx("br", {}), "1. Install the app from the Shopify App Store", _jsx("br", {}), "2. Connect your account", _jsx("br", {}), "3. Configure your settings", _jsx("br", {}), _jsx("br", {}), "Click \"Continue Setup\" to open the app page."] })) : (modalContent.partner?.content) }), modalContent.partner?.app_url && (_jsxs(Text, { as: "p", variant: "bodySm", tone: "subdued", children: ["This will open:", " ", new URL(modalContent.partner.app_url).hostname] }))] }) }) }) })] }));
102
+ };
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import { BannerItem } from "../types";
3
+ export interface PopupBannerProps {
4
+ className?: string;
5
+ onClose?: () => void;
6
+ onAction?: (banner: BannerItem) => void;
7
+ autoShow?: boolean;
8
+ showDelay?: number;
9
+ persistClose?: boolean;
10
+ storageKey?: string;
11
+ }
12
+ export declare const PopupBanner: React.FC<PopupBannerProps>;
@@ -0,0 +1,100 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { usePopupBanner } from "../hooks/useBanner";
4
+ export const PopupBanner = ({ className = "", onClose, onAction, autoShow = true, showDelay = 0, persistClose = true, storageKey = "shopify-dashboard-popup-closed", }) => {
5
+ const banner = usePopupBanner();
6
+ const [isVisible, setIsVisible] = useState(false);
7
+ const [hasBeenClosed, setHasBeenClosed] = useState(false);
8
+ useEffect(() => {
9
+ if (!banner || !autoShow)
10
+ return;
11
+ if (persistClose && typeof window !== "undefined") {
12
+ const closedBanners = localStorage.getItem(storageKey);
13
+ if (closedBanners) {
14
+ const closed = JSON.parse(closedBanners);
15
+ if (closed[banner.id]) {
16
+ setHasBeenClosed(true);
17
+ return;
18
+ }
19
+ }
20
+ }
21
+ const timer = setTimeout(() => {
22
+ setIsVisible(true);
23
+ }, showDelay);
24
+ return () => clearTimeout(timer);
25
+ }, [banner, autoShow, showDelay, persistClose, storageKey]);
26
+ const handleClose = () => {
27
+ setIsVisible(false);
28
+ if (persistClose && banner && typeof window !== "undefined") {
29
+ const closedBanners = localStorage.getItem(storageKey);
30
+ const closed = closedBanners ? JSON.parse(closedBanners) : {};
31
+ closed[banner.id] = true;
32
+ localStorage.setItem(storageKey, JSON.stringify(closed));
33
+ }
34
+ onClose?.();
35
+ };
36
+ const handleAction = () => {
37
+ if (banner) {
38
+ if (banner.link) {
39
+ window.open(banner.link, banner.openType === "new_tab" ? "_blank" : "_self");
40
+ }
41
+ onAction?.(banner);
42
+ }
43
+ };
44
+ if (!banner || hasBeenClosed || !isVisible) {
45
+ return null;
46
+ }
47
+ const styles = {
48
+ backgroundColor: banner.style?.backgroundColor || "#ffffff",
49
+ color: banner.style?.textColor || "#000000",
50
+ };
51
+ const buttonStyles = {
52
+ backgroundColor: banner.style?.buttonColor || "#007bff",
53
+ color: banner.style?.buttonTextColor || "#ffffff",
54
+ };
55
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: `popup-banner-overlay ${className}`, style: {
56
+ position: "fixed",
57
+ top: 0,
58
+ left: 0,
59
+ right: 0,
60
+ bottom: 0,
61
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
62
+ display: "flex",
63
+ alignItems: "center",
64
+ justifyContent: "center",
65
+ zIndex: 9999,
66
+ }, onClick: handleClose }), _jsxs("div", { className: `popup-banner ${className}`, style: {
67
+ ...styles,
68
+ position: "fixed",
69
+ top: "50%",
70
+ left: "50%",
71
+ transform: "translate(-50%, -50%)",
72
+ maxWidth: "500px",
73
+ width: "90%",
74
+ padding: "24px",
75
+ borderRadius: "8px",
76
+ boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
77
+ zIndex: 10000,
78
+ }, onClick: (e) => e.stopPropagation(), children: [_jsx("button", { onClick: handleClose, style: {
79
+ position: "absolute",
80
+ top: "12px",
81
+ right: "12px",
82
+ background: "none",
83
+ border: "none",
84
+ fontSize: "24px",
85
+ cursor: "pointer",
86
+ color: styles.color,
87
+ }, "aria-label": "Close popup", children: "\u00D7" }), banner.icon && (_jsx("div", { style: { marginBottom: "16px" }, children: _jsx("img", { src: banner.icon, alt: "", style: { width: "48px", height: "48px" } }) })), banner.image && (_jsx("div", { style: { marginBottom: "16px" }, children: _jsx("img", { src: banner.image, alt: "", style: { width: "100%", borderRadius: "4px" } }) })), _jsx("h2", { style: { margin: "0 0 12px 0", fontSize: "20px", fontWeight: "bold" }, children: banner.title }), banner.description && (_jsx("p", { style: {
88
+ margin: "0 0 20px 0",
89
+ fontSize: "14px",
90
+ lineHeight: "1.5",
91
+ }, children: banner.description })), banner.link && (_jsx("button", { onClick: handleAction, style: {
92
+ ...buttonStyles,
93
+ padding: "10px 20px",
94
+ border: "none",
95
+ borderRadius: "4px",
96
+ fontSize: "14px",
97
+ fontWeight: "bold",
98
+ cursor: "pointer",
99
+ }, children: banner.linkText || "Learn More" }))] })] }));
100
+ };
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ import { BannerItem } from "../types";
3
+ export interface TopBannerProps {
4
+ className?: string;
5
+ onClose?: () => void;
6
+ onAction?: (banner: BannerItem) => void;
7
+ closable?: boolean;
8
+ sticky?: boolean;
9
+ renderBanner?: (banner: BannerItem, handlers: {
10
+ onClose: () => void;
11
+ onAction: () => void;
12
+ }) => React.ReactNode;
13
+ }
14
+ export declare const TopBanner: React.FC<TopBannerProps>;