@datlv-trustshop/shopify-inapp-components 0.1.12 → 0.1.14

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.
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import { IntegrationItem } from "../types/integration";
3
+ export type IntegrationStatusInfo = {
4
+ isInstalled?: boolean;
5
+ isConnected?: boolean;
6
+ isActive?: boolean;
7
+ isPending?: boolean;
8
+ showUpgradeBadge?: boolean;
9
+ upgradeBadgeText?: string;
10
+ upgradeBadgeIcon?: React.ReactNode;
11
+ upgradeBadgeTooltip?: string;
12
+ upgradeBadgePlan?: "basic" | "pro";
13
+ };
14
+ export type IntegrationStatusProvider = (integration: IntegrationItem) => IntegrationStatusInfo | undefined;
15
+ export interface PartnerIntegrationProps {
16
+ className?: string;
17
+ onManage?: (item: IntegrationItem) => void;
18
+ onInstall?: (item: IntegrationItem) => void;
19
+ onOpen?: (item: IntegrationItem) => void;
20
+ onUpgradeClick?: (item: IntegrationItem) => void;
21
+ renderCard?: (item: IntegrationItem, handlers: {
22
+ onManage: () => void;
23
+ onInstall: () => void;
24
+ onOpen: () => void;
25
+ }) => React.ReactNode;
26
+ maxColumns?: 1 | 2 | 3;
27
+ showEmptyState?: boolean;
28
+ statusProvider?: IntegrationStatusProvider;
29
+ }
30
+ export declare const PartnerIntegration: React.FC<PartnerIntegrationProps>;
31
+ export default PartnerIntegration;
@@ -0,0 +1,176 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Badge, BlockStack, Box, Button, Card, InlineStack, Text, Grid, SkeletonBodyText, EmptyState, } from "@shopify/polaris";
3
+ import { ExternalIcon } from "@shopify/polaris-icons";
4
+ import { usePartnerIntegrations } from "../hooks/usePartnerIntegration";
5
+ import { useDashboardContext } from "../provider/DashboardProvider";
6
+ export const PartnerIntegration = ({ className = "", onManage, onInstall, onOpen, onUpgradeClick, renderCard, maxColumns = 2, showEmptyState = true, statusProvider, }) => {
7
+ const { groups, loading, error } = usePartnerIntegrations();
8
+ const { translations } = useDashboardContext();
9
+ const handleManage = (item) => {
10
+ const url = item.button_manage_url;
11
+ if (url) {
12
+ // Check if it's an internal route (starts with /)
13
+ if (url.startsWith("/")) {
14
+ // Let the parent handle internal navigation
15
+ onManage?.(item);
16
+ }
17
+ else {
18
+ window.open(url, "_blank");
19
+ onOpen?.(item);
20
+ }
21
+ }
22
+ else {
23
+ onManage?.(item);
24
+ }
25
+ };
26
+ const handleInstall = (item) => {
27
+ const url = item.button_install_link || item.button_install_url || item.app_url;
28
+ if (url) {
29
+ window.open(url, "_blank");
30
+ }
31
+ onInstall?.(item);
32
+ };
33
+ const renderIntegrationCard = (item) => {
34
+ if (renderCard) {
35
+ return renderCard(item, {
36
+ onManage: () => handleManage(item),
37
+ onInstall: () => handleInstall(item),
38
+ onOpen: () => {
39
+ if (item.app_url) {
40
+ window.open(item.app_url, "_blank");
41
+ }
42
+ onOpen?.(item);
43
+ },
44
+ });
45
+ }
46
+ // Get status from provider or use defaults
47
+ const status = statusProvider?.(item);
48
+ // Determine badge based on status or integration key
49
+ const getBadgeInfo = () => {
50
+ // Use external status if provided
51
+ if (status) {
52
+ if (status.isConnected) {
53
+ return {
54
+ text: translations.partnerIntegration?.connected || "Connected",
55
+ tone: "success",
56
+ };
57
+ }
58
+ if (status.isActive) {
59
+ return {
60
+ text: translations.partnerIntegration?.active || "Active",
61
+ tone: "success",
62
+ };
63
+ }
64
+ if (status.isInstalled) {
65
+ return {
66
+ text: translations.partnerIntegration?.installed || "Installed",
67
+ tone: "info",
68
+ };
69
+ }
70
+ if (status.isPending) {
71
+ return {
72
+ text: "Pending",
73
+ tone: "warning",
74
+ };
75
+ }
76
+ // If status explicitly says inactive
77
+ if (status.isActive === false) {
78
+ return {
79
+ text: translations.partnerIntegration?.inactive || "Inactive",
80
+ tone: undefined,
81
+ };
82
+ }
83
+ }
84
+ // Fallback to default logic based on key
85
+ if (item.key === "google_reviews") {
86
+ return {
87
+ text: translations.partnerIntegration?.connected || "Connected",
88
+ tone: "success",
89
+ };
90
+ }
91
+ if (item.key === "after_ship") {
92
+ // AfterShip shows both buttons, so it's likely installed but inactive by default
93
+ return {
94
+ text: translations.partnerIntegration?.inactive || "Inactive",
95
+ tone: undefined,
96
+ };
97
+ }
98
+ // Check if installed based on having both manage and install buttons
99
+ // Items with both buttons are typically installed but may be inactive
100
+ if (item.button_manage_text && item.button_install_text) {
101
+ return {
102
+ text: translations.partnerIntegration?.inactive || "Inactive",
103
+ tone: undefined,
104
+ };
105
+ }
106
+ // Only manage button = installed and active
107
+ if (item.button_manage_text && !item.button_install_text) {
108
+ return {
109
+ text: translations.partnerIntegration?.active || "Active",
110
+ tone: "success",
111
+ };
112
+ }
113
+ return null;
114
+ };
115
+ const badgeInfo = getBadgeInfo();
116
+ return (_jsx(Card, { children: _jsx(InlineStack, { gap: "300", children: _jsxs(InlineStack, { gap: "300", wrap: false, children: [item.icon_url && (_jsx("img", { src: item.icon_url, alt: item.title, style: {
117
+ width: "40px",
118
+ height: "40px",
119
+ borderRadius: "8px",
120
+ } })), _jsx(Box, { children: _jsxs(BlockStack, { gap: "300", children: [_jsxs(BlockStack, { gap: "100", children: [_jsxs(InlineStack, { gap: "200", align: "start", children: [_jsx(Text, { as: "p", variant: "bodyLg", fontWeight: "bold", children: item.title }), badgeInfo && (_jsx(Badge, { tone: badgeInfo.tone, children: badgeInfo.text })), status?.showUpgradeBadge && (_jsxs("div", { onClick: () => onUpgradeClick?.(item), style: {
121
+ cursor: "pointer",
122
+ display: "inline-flex",
123
+ alignItems: "center",
124
+ padding: "4px 6px",
125
+ gap: "4px",
126
+ borderRadius: "var(--p-border-radius-150)",
127
+ backgroundColor: status.upgradeBadgePlan === "pro"
128
+ ? "#F0F2FF"
129
+ : "#FFF1E3",
130
+ color: status.upgradeBadgePlan === "pro"
131
+ ? "#7126FF"
132
+ : "#4F4700",
133
+ fontSize: "12px",
134
+ fontWeight: 500,
135
+ }, title: status.upgradeBadgeTooltip, children: [status.upgradeBadgeIcon && (_jsx("span", { style: {
136
+ display: "inline-flex",
137
+ alignItems: "center",
138
+ }, children: status.upgradeBadgeIcon })), _jsx("span", { children: status.upgradeBadgeText || "Upgrade" })] }))] }), item.content && (_jsx(Text, { as: "p", variant: "bodyMd", tone: "subdued", children: item.content }))] }), _jsx(InlineStack, { gap: "200", children: status ? (
139
+ // Status provider exists, use it for button logic
140
+ status.isConnected ? (
141
+ // Connected: show manage button
142
+ _jsx(Button, { onClick: () => handleManage(item), children: item.button_manage_text ||
143
+ translations.partnerIntegration?.manage ||
144
+ "Manage" })) : status.isInstalled ? (
145
+ // Installed but not connected
146
+ _jsxs(_Fragment, { children: [item.button_manage_text && (_jsx(Button, { onClick: () => handleManage(item), children: item.button_manage_text })), item.button_install_text && (_jsx(Button, { icon: ExternalIcon, onClick: () => handleInstall(item), children: item.button_install_text }))] })) : (
147
+ // Not installed: show install button
148
+ item.button_install_text && (_jsx(Button, { icon: ExternalIcon, onClick: () => handleInstall(item), children: item.button_install_text })))) : (
149
+ // No status provider, show all available buttons from API
150
+ _jsxs(_Fragment, { children: [item.button_manage_text && (_jsx(Button, { onClick: () => handleManage(item), children: item.button_manage_text })), item.button_install_text && (_jsx(Button, { icon: ExternalIcon, onClick: () => handleInstall(item), children: item.button_install_text }))] })) })] }) })] }) }) }, `integration--${item.id || item.key}`));
151
+ };
152
+ // Loading state
153
+ if (loading) {
154
+ return (_jsx("div", { className: className, children: _jsx(BlockStack, { gap: "600", children: [1, 2, 3].map((groupIndex) => (_jsxs(BlockStack, { gap: "400", children: [_jsx(Box, { paddingInlineStart: { xs: "200", md: "0" }, children: _jsx(SkeletonBodyText, { lines: 1 }) }), _jsx(Grid, { children: [1, 2].map((cardIndex) => (_jsx(Grid.Cell, { columnSpan: { xs: 6, md: maxColumns === 1 ? 6 : 6 }, children: _jsx(Card, { children: _jsx(BlockStack, { gap: "300", children: _jsx(SkeletonBodyText, { lines: 3 }) }) }) }, `skeleton-card-${groupIndex}-${cardIndex}`))) })] }, `skeleton-group-${groupIndex}`))) }) }));
155
+ }
156
+ // Error state
157
+ if (error) {
158
+ return (_jsx("div", { className: className, children: _jsx(Card, { children: _jsx(EmptyState, { heading: translations.partnerIntegration?.errorTitle ||
159
+ "Unable to load integrations", image: "", children: _jsx("p", { children: translations.partnerIntegration?.errorMessage ||
160
+ "Please try again later" }) }) }) }));
161
+ }
162
+ // Empty state
163
+ if (groups.length === 0) {
164
+ if (!showEmptyState)
165
+ return null;
166
+ return (_jsx("div", { className: className, children: _jsx(Card, { children: _jsx(EmptyState, { heading: translations.partnerIntegration?.noData ||
167
+ "No integrations available", image: "", children: _jsx("p", { children: translations.partnerIntegration?.noDataMessage ||
168
+ "Check back later for available integrations" }) }) }) }));
169
+ }
170
+ // Render groups
171
+ return (_jsx("div", { className: className, children: _jsx(BlockStack, { gap: "600", children: groups.map((group) => (_jsxs(BlockStack, { gap: "400", children: [_jsx(Box, { paddingInlineStart: { xs: "200", md: "0" }, children: _jsx(Text, { as: "h3", variant: "headingMd", fontWeight: "semibold", children: group.title }) }), _jsx(Grid, { children: group.items.map((item) => (_jsx(Grid.Cell, { columnSpan: {
172
+ xs: 6,
173
+ md: maxColumns === 1 ? 6 : maxColumns === 3 ? 2 : 3,
174
+ }, children: renderIntegrationCard(item) }, `integration-cell--${item.id || item.key}`))) })] }, `integration-group--${group.key}`))) }) }));
175
+ };
176
+ export default PartnerIntegration;
@@ -1,13 +1,38 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from "react";
2
+ import { useState, useEffect } from "react";
3
3
  import { Banner, Button, Text, BlockStack, InlineStack, } from "@shopify/polaris";
4
4
  import { ExternalIcon } from "@shopify/polaris-icons";
5
5
  import { useTopBanner } from "../hooks/useBanner";
6
6
  export const TopBanner = ({ className = "", onClose, onAction, closable = true, renderBanner, }) => {
7
7
  const banner = useTopBanner();
8
- const [isVisible, setIsVisible] = useState(true);
8
+ const [isDismissed, setIsDismissed] = useState(false);
9
+ const DISMISS_KEY = "ts-top-banner-dismissed";
10
+ const DISMISS_DURATION = 7 * 24 * 60 * 60 * 1000;
11
+ useEffect(() => {
12
+ const dismissedData = localStorage.getItem(DISMISS_KEY);
13
+ if (dismissedData) {
14
+ try {
15
+ const { timestamp } = JSON.parse(dismissedData);
16
+ const now = Date.now();
17
+ if (now - timestamp < DISMISS_DURATION) {
18
+ setIsDismissed(true);
19
+ }
20
+ else {
21
+ localStorage.removeItem(DISMISS_KEY);
22
+ }
23
+ }
24
+ catch (error) {
25
+ localStorage.removeItem(DISMISS_KEY);
26
+ }
27
+ }
28
+ }, []);
9
29
  const handleClose = () => {
10
- setIsVisible(false);
30
+ const dismissData = {
31
+ timestamp: Date.now(),
32
+ dismissed: true,
33
+ };
34
+ localStorage.setItem(DISMISS_KEY, JSON.stringify(dismissData));
35
+ setIsDismissed(true);
11
36
  onClose?.();
12
37
  };
13
38
  const handleAction = () => {
@@ -18,7 +43,7 @@ export const TopBanner = ({ className = "", onClose, onAction, closable = true,
18
43
  onAction?.(banner);
19
44
  }
20
45
  };
21
- if (!banner || !isVisible) {
46
+ if (!banner || isDismissed) {
22
47
  return null;
23
48
  }
24
49
  if (renderBanner) {
@@ -7,3 +7,4 @@ export * from "./ArticleSlide";
7
7
  export * from "./WhatsNew";
8
8
  export * from "./GrowApps";
9
9
  export * from "./PartnerList";
10
+ export * from "./PartnerIntegration";
@@ -7,3 +7,4 @@ export * from "./ArticleSlide";
7
7
  export * from "./WhatsNew";
8
8
  export * from "./GrowApps";
9
9
  export * from "./PartnerList";
10
+ export * from "./PartnerIntegration";
@@ -16,6 +16,8 @@ export function adaptDashboardData(rawData) {
16
16
  const whatsNew = data.whats_new?.product_update || [];
17
17
  const growApps = data.grow_apps || [];
18
18
  const growAppsDisplayLimit = data.grow_apps_display_limit;
19
+ // Keep the raw integrations data for the Integration component
20
+ const integrations = data.integrations || [];
19
21
  return {
20
22
  banners: adaptBanners(banners),
21
23
  apps: adaptApps(allApps),
@@ -23,6 +25,7 @@ export function adaptDashboardData(rawData) {
23
25
  whatsNew: adaptProductUpdates(whatsNew),
24
26
  grow_apps: growApps,
25
27
  grow_apps_display_limit: growAppsDisplayLimit,
28
+ integrations: integrations, // Add raw integrations
26
29
  };
27
30
  }
28
31
  function adaptBanners(banners) {
@@ -4,3 +4,4 @@ export * from "./useApps";
4
4
  export * from "./useArticles";
5
5
  export * from "./useWhatsNew";
6
6
  export * from "./useGrowApps";
7
+ export * from "./usePartnerIntegration";
@@ -4,3 +4,4 @@ export * from "./useApps";
4
4
  export * from "./useArticles";
5
5
  export * from "./useWhatsNew";
6
6
  export * from "./useGrowApps";
7
+ export * from "./usePartnerIntegration";
@@ -0,0 +1,22 @@
1
+ import { IntegrationItem, IntegrationGroup } from "../types/integration";
2
+ /**
3
+ * Hook to get all partner integrations from dashboard data
4
+ */
5
+ export declare function usePartnerIntegrations(): {
6
+ groups: IntegrationGroup[];
7
+ loading: boolean;
8
+ error: Error | null;
9
+ integrations: any[];
10
+ };
11
+ /**
12
+ * Hook to get a specific integration by key
13
+ */
14
+ export declare function usePartnerIntegrationByKey(key: string): IntegrationItem | null;
15
+ /**
16
+ * Hook to get integration status
17
+ */
18
+ export declare function usePartnerIntegrationStatus(key: string): {
19
+ isConnected: boolean;
20
+ isActive: boolean;
21
+ isInstalled: boolean;
22
+ };
@@ -0,0 +1,69 @@
1
+ import { useMemo } from "react";
2
+ import { useDashboardContext } from "../provider/DashboardProvider";
3
+ /**
4
+ * Hook to get all partner integrations from dashboard data
5
+ */
6
+ export function usePartnerIntegrations() {
7
+ const { state, translations } = useDashboardContext();
8
+ const groups = useMemo(() => {
9
+ const integrations = state.data?.integrations || [];
10
+ const groupsMap = [];
11
+ // Group by category_key
12
+ const reviewSources = integrations.filter((item) => item.category_key === "review_sources");
13
+ if (reviewSources.length > 0) {
14
+ groupsMap.push({
15
+ title: translations.partnerIntegration?.reviewSourcesTitle || "Review Sources",
16
+ key: "review_sources",
17
+ items: reviewSources.sort((a, b) => a.position - b.position),
18
+ });
19
+ }
20
+ const postPurchase = integrations.filter((item) => item.category_key === "post_purchase_automation");
21
+ if (postPurchase.length > 0) {
22
+ groupsMap.push({
23
+ title: translations.partnerIntegration?.postPurchaseTitle || "Post-purchase & Automation",
24
+ key: "post_purchase_automation",
25
+ items: postPurchase.sort((a, b) => a.position - b.position),
26
+ });
27
+ }
28
+ const seoSnippets = integrations.filter((item) => item.category_key === "seo_rich_snippets");
29
+ if (seoSnippets.length > 0) {
30
+ groupsMap.push({
31
+ title: translations.partnerIntegration?.seoSnippetsTitle || "SEO & Rich Snippets",
32
+ key: "seo_rich_snippets",
33
+ items: seoSnippets.sort((a, b) => a.position - b.position),
34
+ });
35
+ }
36
+ return groupsMap;
37
+ }, [state.data?.integrations, translations]);
38
+ return {
39
+ groups,
40
+ loading: state.loading,
41
+ error: state.error,
42
+ integrations: state.data?.integrations || [],
43
+ };
44
+ }
45
+ /**
46
+ * Hook to get a specific integration by key
47
+ */
48
+ export function usePartnerIntegrationByKey(key) {
49
+ const { integrations } = usePartnerIntegrations();
50
+ return useMemo(() => {
51
+ return integrations.find((item) => item.key === key) || null;
52
+ }, [integrations, key]);
53
+ }
54
+ /**
55
+ * Hook to get integration status
56
+ */
57
+ export function usePartnerIntegrationStatus(key) {
58
+ const integration = usePartnerIntegrationByKey(key);
59
+ return useMemo(() => {
60
+ if (!integration) {
61
+ return { isConnected: false, isActive: false, isInstalled: false };
62
+ }
63
+ // Check based on integration key and button states
64
+ const isConnected = key === 'google_reviews';
65
+ const isActive = key !== 'after_ship';
66
+ const isInstalled = !integration.button_install_text;
67
+ return { isConnected, isActive, isInstalled };
68
+ }, [integration, key]);
69
+ }
@@ -156,6 +156,10 @@ export const DashboardProvider = ({ children, config, locale, translations, auto
156
156
  ...defaultTranslations.banner,
157
157
  ...translations.banner,
158
158
  },
159
+ partnerIntegration: {
160
+ ...defaultTranslations.partnerIntegration,
161
+ ...translations.partnerIntegration,
162
+ },
159
163
  };
160
164
  }, [translations]);
161
165
  // Get engine from manager (might be null initially)
@@ -24,4 +24,22 @@ export const defaultTranslations = {
24
24
  learnMore: "Learn more",
25
25
  dismiss: "Dismiss",
26
26
  },
27
+ partnerIntegration: {
28
+ title: "Partner Integrations",
29
+ reviewSourcesTitle: "Review Sources",
30
+ postPurchaseTitle: "Post-purchase & Automation",
31
+ seoSnippetsTitle: "SEO & Rich Snippets",
32
+ noData: "No integrations available",
33
+ noDataMessage: "Check back later for available integrations",
34
+ errorTitle: "Unable to load integrations",
35
+ errorMessage: "Please try again later",
36
+ install: "Install app",
37
+ installed: "Installed",
38
+ manage: "Manage",
39
+ open: "Open",
40
+ connect: "Connect",
41
+ connected: "Connected",
42
+ inactive: "Inactive",
43
+ active: "Active"
44
+ }
27
45
  };
@@ -25,6 +25,7 @@ export interface DashboardData {
25
25
  grow_apps?: GrowApp[];
26
26
  grow_apps_display_limit?: number;
27
27
  partner_list?: PartnerItem[];
28
+ integrations?: any[];
28
29
  }
29
30
  export interface DashboardConfig {
30
31
  apiUrl: string;
@@ -4,3 +4,4 @@ export * from "./article";
4
4
  export * from "./product-update";
5
5
  export * from "./dashboard";
6
6
  export * from "./partner";
7
+ export * from "./integration";
@@ -4,3 +4,4 @@ export * from "./article";
4
4
  export * from "./product-update";
5
5
  export * from "./dashboard";
6
6
  export * from "./partner";
7
+ export * from "./integration";
@@ -0,0 +1,33 @@
1
+ export type IntegrationCategory = "review_sources" | "post_purchase_automation" | "seo_rich_snippets";
2
+ export type IntegrationStatus = "connected" | "active" | "inactive";
3
+ export interface IntegrationItem {
4
+ id?: string | number;
5
+ key?: string;
6
+ title: string;
7
+ badge_text?: string;
8
+ sub_title?: string;
9
+ content?: string;
10
+ icon_url?: string;
11
+ category?: IntegrationCategory;
12
+ category_key?: string;
13
+ status?: IntegrationStatus;
14
+ installed?: boolean;
15
+ button_text?: string;
16
+ button_install_text?: string | null;
17
+ button_install_link?: string | null;
18
+ button_install_url?: string;
19
+ button_manage_text?: string | null;
20
+ button_manage_url?: string | null;
21
+ button_connect_text?: string;
22
+ button_connect_url?: string;
23
+ app_url?: string;
24
+ position: number;
25
+ requires_plan?: string;
26
+ source?: string;
27
+ group_type?: string;
28
+ }
29
+ export interface IntegrationGroup {
30
+ title: string;
31
+ key: IntegrationCategory;
32
+ items: IntegrationItem[];
33
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -24,5 +24,23 @@ export interface SDKTranslations {
24
24
  learnMore?: string;
25
25
  dismiss?: string;
26
26
  };
27
+ partnerIntegration?: {
28
+ title?: string;
29
+ reviewSourcesTitle?: string;
30
+ postPurchaseTitle?: string;
31
+ seoSnippetsTitle?: string;
32
+ noData?: string;
33
+ noDataMessage?: string;
34
+ errorTitle?: string;
35
+ errorMessage?: string;
36
+ install?: string;
37
+ installed?: string;
38
+ manage?: string;
39
+ open?: string;
40
+ connect?: string;
41
+ connected?: string;
42
+ inactive?: string;
43
+ active?: string;
44
+ };
27
45
  }
28
46
  export type TranslationKey = keyof SDKTranslations;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datlv-trustshop/shopify-inapp-components",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "private": false,
5
5
  "description": "React TypeScript components for Shopify in-app dashboard content",
6
6
  "main": "dist/index.js",