From 425701d6e0c10a114c1617e96af78e94afcf5fdd Mon Sep 17 00:00:00 2001 From: Larris Xie Date: Thu, 17 Jul 2025 10:52:52 -0400 Subject: [PATCH 1/5] Adds safe management tutorial for cash management POS UI extensions --- .../cash-management-example/SafeModal.tsx | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/SafeModal.tsx diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/SafeModal.tsx b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/SafeModal.tsx new file mode 100644 index 0000000000..f6fe9162f4 --- /dev/null +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/SafeModal.tsx @@ -0,0 +1,214 @@ +import React, {useState, useEffect} from 'react'; + +import { + Text, + Screen, + ScrollView, + reactExtension, + useApi, + Button, + Stack, + Section, + TextField, + List, + ListRowSubtitle, +} from '@shopify/ui-extensions-react/point-of-sale'; + +import type {Storage} from '@shopify/ui-extensions/point-of-sale'; + +// [START safe-modal.data-interfaces] +// 2. Define the interfaces for storing safe management data +interface SafeActivity { + id: number; + type: string; + amount: number; + timestamp: Date; +} + +interface SafeDetails { + balance: number; + activities: SafeActivity[]; +} +// [END safe-modal.data-interfaces] + +// [START safe-modal.component] +// 3. Implement the `SafeModal` component +const SafeModal = () => { + const [activityType, setActivityType] = useState('deposit'); + const [amount, setAmount] = useState(''); + // [END safe-modal.component] + + // [START safe-modal.storage-setup] + // 4. Setup the api and storage + const api = useApi<'pos.product-details.action.render'>(); + const storage: Storage = api.storage; + + // 5. Setup the states for the safe management data + const [balance, setBalance] = useState(0); + const [activities, setActivities] = useState([]); + // [END safe-modal.storage-setup] + + // [START safe-modal.load-data] + // 6. Load the data from the storage + useEffect(() => { + const loadData = async () => { + try { + const storedBalance = (await storage.get('balance')) || 0; + const storedActivities = (await storage.get('activities')) || []; + + setBalance(storedBalance); + setActivities(storedActivities); + } catch (error) { + console.error('Error loading data from storage:', error); + } + }; + + loadData(); + }, [storage]); + // [END safe-modal.load-data] + + // [START safe-modal.handle-activity] + // 7. Implement deposit and withdrawal logic + const handleActivity = async () => { + const activityAmount = parseFloat(amount); + + try { + let newBalance = balance; + if (activityType === 'deposit') { + newBalance = balance + activityAmount; + } else if (activityType === 'withdrawal') { + newBalance = balance - activityAmount; + } + setBalance(newBalance); + await storage.set('balance', newBalance); + + const newActivity: SafeActivity = { + id: activities.length + 1, + type: activityType, + amount: activityAmount, + timestamp: new Date(), + }; + const updatedActivities = [...activities, newActivity]; + setActivities(updatedActivities); + await storage.set('activities', updatedActivities); + + setAmount(''); + } catch (error) { + console.error('Error saving activity:', error); + } + }; + // [END safe-modal.handle-activity] + + // [START safe-modal.validation] + // 8. Check if the activity amount is valid + const canSubmit = () => { + const activityAmount = parseFloat(amount); + if (isNaN(activityAmount) || activityAmount <= 0) return false; + if (activityType === 'withdrawal' && activityAmount > balance) return false; + return true; + }; + // [END safe-modal.validation] + + // [START safe-modal.format-activities] + // 9. Format the activities in to a list + const activityListData = (activities || []).map((activity: SafeActivity) => ({ + id: activity.id.toString(), + leftSide: { + label: `$${(activity.amount || 0).toFixed(2)}`, + subtitle: [ + { + content: activity.type === 'deposit' ? 'Deposit' : 'Withdrawal', + }, + ] as [ListRowSubtitle], + }, + rightSide: { + label: new Date(activity.timestamp).toLocaleString(), + }, + })); + // [END safe-modal.format-activities] + + // 10. Render the SafeModal component to use the safe management solution + return ( + + + {/* [START safe-modal.overview-section] */} +
+ + Current Balance + ${(balance || 0).toFixed(2)} + + + Activities Count + {activities.length} + +
+ {/* [END safe-modal.overview-section] */} + + {/* [START safe-modal.activity-form] */} +
+ + +
+ {/* [END safe-modal.activity-form] */} + + {/* [START safe-modal.activities-list] */} +
+ + {/* [START safe-modal.clear-activities] */} +
+ {/* [END safe-modal.activities-list] */} +
+
+ ); +}; + +// [START safe-modal.render-extension] +// 1. Render the SafeModal component at the `pos.product-details.action.render` target +export default reactExtension('pos.product-details.action.render', () => ( + +)); +// [END safe-modal.render-extension] From babf1613057c10fa227b3a607b7f888f723e4037 Mon Sep 17 00:00:00 2001 From: Larris Xie Date: Thu, 17 Jul 2025 10:59:52 -0400 Subject: [PATCH 2/5] Adds shopify.extension.toml for cash management tutorial --- .../shopify.extension.toml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/shopify.extension.toml diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/shopify.extension.toml b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/shopify.extension.toml new file mode 100644 index 0000000000..d6a92e1a1b --- /dev/null +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/shopify.extension.toml @@ -0,0 +1,28 @@ +# The version of APIs your extension will receive. Learn more: +# https://shopify.dev/docs/api/usage/versioning +api_version = "unstable" + +[[extensions]] +type = "ui_extension" +name = "cash-management-extension" + +handle = "cash-management-extension" +description = "Safe Management" + +# Controls where in POS your extension will be injected, +# and the file that contains your extension’s source code. +[[extensions.targeting]] +module = "./src/SafeModal.tsx" +target = "pos.cash-session-details.action.render" + +[[extensions.targeting]] +module = "./src/BannerAlert.tsx" +target = "pos.cash-session-details.banner.render" + +[[extensions.targeting]] +module = "./src/MenuItem.tsx" +target = "pos.cash-session-details.action.menu-item.render" + +[[extensions.targeting]] +module = "./src/InStoreCashInfo.tsx" +target = "pos.cash-session-summary.block.render" \ No newline at end of file From 2c90b520a66ff5381e46b797c5c1bdd783ab9788 Mon Sep 17 00:00:00 2001 From: Larris Xie Date: Thu, 17 Jul 2025 16:48:34 -0400 Subject: [PATCH 3/5] Adds business rules and banner target tutorial --- .../cash-management-example/BannerAlert.tsx | 51 +++++ .../cash-management-example/SafeModal.tsx | 205 +++++++++++------- .../useBusinessRules.ts | 110 ++++++++++ 3 files changed, 285 insertions(+), 81 deletions(-) create mode 100644 packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/BannerAlert.tsx create mode 100644 packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/useBusinessRules.ts diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/BannerAlert.tsx b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/BannerAlert.tsx new file mode 100644 index 0000000000..b5787dfc25 --- /dev/null +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/BannerAlert.tsx @@ -0,0 +1,51 @@ +import React, {useState, useEffect} from 'react'; + +import { + reactExtension, + Banner, + useApi, + Text, +} from '@shopify/ui-extensions-react/point-of-sale'; +import {useBusinessRules} from './useBusinessRules'; + +// [START banner-alert.component] +// 2. Implement the `BannerAlert` component +const BannerAlert = () => { + // [END banner-alert.component] + // [START banner-alert.api] + // 3. Setup the api + const api = useApi<'pos.cash-session-details.banner.render'>(); + // [END banner-alert.api] + + // [START banner-alert.use-business-rules] + // 4. Check if any business rules are violated using the useBusinessRules hook + const [deviceId, setDeviceId] = useState(''); + useEffect(() => { + api.device.getDeviceId().then(setDeviceId); + }, []); + const {isViolated, alertMessage, loading} = useBusinessRules(deviceId); + // [END banner-alert.use-business-rules] + + // [START banner-alert.loading-state] + // 5. Handle error and loading states + if (loading) { + return Loading...; + } + // [END banner-alert.loading-state] + + // [START banner-alert.render-implementation] + // 6. Display an alert banner if a business rule is violated + if (isViolated) { + return ; + } + + return null; + // [END banner-alert.render-implementation] +}; + +// [START banner-alert.render-extension] +// 1. Render the BannerAlert component at the `pos.cash-session-details.banner.render` target +export default reactExtension('pos.cash-session-details.banner.render', () => ( + +)); +// [END banner-alert.render-extension] diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/SafeModal.tsx b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/SafeModal.tsx index f6fe9162f4..e33c5e775f 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/SafeModal.tsx +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/SafeModal.tsx @@ -8,21 +8,23 @@ import { useApi, Button, Stack, - Section, TextField, List, ListRowSubtitle, + Navigator, + SectionHeader, } from '@shopify/ui-extensions-react/point-of-sale'; import type {Storage} from '@shopify/ui-extensions/point-of-sale'; // [START safe-modal.data-interfaces] -// 2. Define the interfaces for storing safe management data +// 2. Define the data structures for safe activity data and safe details. interface SafeActivity { id: number; type: string; amount: number; timestamp: Date; + staffName?: string; } interface SafeDetails { @@ -36,20 +38,19 @@ interface SafeDetails { const SafeModal = () => { const [activityType, setActivityType] = useState('deposit'); const [amount, setAmount] = useState(''); + const [balance, setBalance] = useState(0); + const [activities, setActivities] = useState([]); // [END safe-modal.component] - // [START safe-modal.storage-setup] + // [START safe-modal.api-storage] // 4. Setup the api and storage - const api = useApi<'pos.product-details.action.render'>(); + const api = useApi<'pos.cash-session-details.action.render'>(); const storage: Storage = api.storage; - - // 5. Setup the states for the safe management data - const [balance, setBalance] = useState(0); - const [activities, setActivities] = useState([]); - // [END safe-modal.storage-setup] + const {staffMemberId} = api.session.currentSession; + // [END safe-modal.api-storage] // [START safe-modal.load-data] - // 6. Load the data from the storage + // 5. Intalize or load the data from the storage useEffect(() => { const loadData = async () => { try { @@ -68,31 +69,30 @@ const SafeModal = () => { // [END safe-modal.load-data] // [START safe-modal.handle-activity] - // 7. Implement deposit and withdrawal logic + // 6. Implement deposit and withdraw logic const handleActivity = async () => { const activityAmount = parseFloat(amount); try { - let newBalance = balance; if (activityType === 'deposit') { - newBalance = balance + activityAmount; - } else if (activityType === 'withdrawal') { - newBalance = balance - activityAmount; + setBalance(balance + activityAmount); + } else if (activityType === 'withdraw') { + setBalance(balance - activityAmount); } - setBalance(newBalance); - await storage.set('balance', newBalance); + await storage.set('balance', balance); const newActivity: SafeActivity = { id: activities.length + 1, type: activityType, amount: activityAmount, timestamp: new Date(), + staffName: staffMemberId?.toString(), }; - const updatedActivities = [...activities, newActivity]; - setActivities(updatedActivities); - await storage.set('activities', updatedActivities); + setActivities([...activities, newActivity]); + await storage.set('activities', activities); setAmount(''); + api.navigation.pop(); } catch (error) { console.error('Error saving activity:', error); } @@ -100,76 +100,135 @@ const SafeModal = () => { // [END safe-modal.handle-activity] // [START safe-modal.validation] - // 8. Check if the activity amount is valid + // 7. Check if the activity amount is valid const canSubmit = () => { const activityAmount = parseFloat(amount); if (isNaN(activityAmount) || activityAmount <= 0) return false; - if (activityType === 'withdrawal' && activityAmount > balance) return false; + if (activityType === 'withdraw' && activityAmount > balance) return false; return true; }; // [END safe-modal.validation] // [START safe-modal.format-activities] - // 9. Format the activities in to a list + // 8. Format the activities into a list const activityListData = (activities || []).map((activity: SafeActivity) => ({ id: activity.id.toString(), leftSide: { - label: `$${(activity.amount || 0).toFixed(2)}`, + label: new Date(activity.timestamp).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + }), subtitle: [ { - content: activity.type === 'deposit' ? 'Deposit' : 'Withdrawal', + content: activity.staffName, }, ] as [ListRowSubtitle], }, rightSide: { - label: new Date(activity.timestamp).toLocaleString(), + label: `${activity.type === 'deposit' ? '+' : '-'} $${( + activity.amount || 0 + ).toFixed(2)}`, }, })); // [END safe-modal.format-activities] - // 10. Render the SafeModal component to use the safe management solution + // [START safe-modal.format-overview] + // 9. Format the safe overview into a list + const overviewListData = [ + { + id: 'current-balance', + leftSide: { + label: 'Current balance:', + }, + rightSide: { + label: `$${(balance || 0).toFixed(2)}`, + }, + }, + { + id: 'last-activity', + leftSide: { + label: 'Last activity:', + }, + rightSide: { + label: + activities.length > 0 + ? new Date( + activities[activities.length - 1].timestamp, + ).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + }) + : 'No activities yet', + }, + }, + ]; + // [END safe-modal.format-overview] + + // [START safe-modal.render-ui] + // 10. Render the SafeModal component to display the safe management solution return ( - - - {/* [START safe-modal.overview-section] */} -
- - Current Balance - ${(balance || 0).toFixed(2)} - - - Activities Count - {activities.length} - -
- {/* [END safe-modal.overview-section] */} - - {/* [START safe-modal.activity-form] */} -
- - -
- {/* [END safe-modal.activity-form] */} - - {/* [START safe-modal.activities-list] */} -
- - {/* [START safe-modal.clear-activities] */} -
- {/* [END safe-modal.activities-list] */} -
-
+ + + ); }; +// [END safe-modal.render-ui] // [START safe-modal.render-extension] -// 1. Render the SafeModal component at the `pos.product-details.action.render` target -export default reactExtension('pos.product-details.action.render', () => ( +// 1. Render the SafeModal component at the `pos.cash-session-details.action.render` target +export default reactExtension('pos.cash-session-details.action.render', () => ( )); // [END safe-modal.render-extension] diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/useBusinessRules.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/useBusinessRules.ts new file mode 100644 index 0000000000..95c323f3b8 --- /dev/null +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/useBusinessRules.ts @@ -0,0 +1,110 @@ +import {useEffect, useState} from 'react'; + +// [START use-business-rules.define-thresholds] +// 1. Define business rules, let's use a min/max cash threshold as an example +type BusinessRuleViolation = { + isViolated: boolean; + message?: string; + ruleType?: string; +}; + +const MIN_CASH_THRESHOLD = 100; +const MAX_CASH_THRESHOLD = 1000; +// [END use-business-rules.define-thresholds] + +// [START use-business-rules.direct-api] +// 2. Fetch the amount of cash in the drawer currently associated with the POS device +export const fetchDrawerAmount = async (pointOfSaleDeviceId: string) => { + const result = await fetch('shopify:admin/api/graphql.json', { + method: 'POST', + body: JSON.stringify({ + query: ` + TBD + `, + variables: { + pointOfSaleDeviceId: `gid://shopify/PointOfSaleDevice/${pointOfSaleDeviceId}`, + }, + }), + }); + + const json = await result.json(); + + if (json.errors) { + console.error('GraphQL Errors:', json.errors); + json.errors.forEach((error: any) => { + console.error('GraphQL Error Details:', error); + }); + return null; + } + + if (!result.ok) { + console.error('Network Error:', result.statusText); + return null; + } + + return json.data; +}; +// [END use-business-rules.direct-api] + +// [START use-business-rules.hook] +// 3. Implement the useBusinessRules hook +export const useBusinessRules = (pointOfSaleDeviceId: string) => { + const [isViolated, setIsViolated] = useState(false); + const [alertMessage, setAlertMessage] = useState(''); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function checkRules() { + try { + const result = await checkDrawerAmount(pointOfSaleDeviceId); + setIsViolated(result.isViolated); + + if (result.isViolated) { + setAlertMessage(result.message || ''); + } + setLoading(false); + } catch (error) { + console.error(error); + setLoading(false); + } + } + checkRules(); + }, [pointOfSaleDeviceId]); + + return { + isViolated, + alertMessage, + loading, + }; +}; +// [END use-business-rules.hook] + +// [START use-business-rules.check-drawer-amount] +// 4. Check if the drawer amount is within the min/max threshold +export const checkDrawerAmount = async ( + pointOfSaleDeviceId: string, +): Promise => { + const drawerAmount = await fetchDrawerAmount(pointOfSaleDeviceId); + if (drawerAmount < MIN_CASH_THRESHOLD) { + return { + isViolated: true, + ruleType: 'drawer_amount', + message: `Drawer amount is $${ + MIN_CASH_THRESHOLD - drawerAmount + } below the minimum threshold`, + }; + } + if (drawerAmount > MAX_CASH_THRESHOLD) { + return { + isViolated: true, + ruleType: 'drawer_amount', + message: `Drawer amount is $${ + drawerAmount - MAX_CASH_THRESHOLD + } above the maximum threshold`, + }; + } + return { + isViolated: false, + }; +}; +// [END use-business-rules.check-drawer-amount] From 0d8a346838eb10aceb17728f28553807082bcb96 Mon Sep 17 00:00:00 2001 From: Larris Xie Date: Wed, 23 Jul 2025 11:46:22 -0400 Subject: [PATCH 4/5] Fixes formatting and wording --- .../cash-management-example/BannerAlert.tsx | 5 +- .../cash-management-example/SafeModal.tsx | 2 +- .../useBusinessRules.ts | 62 +++++++++---------- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/BannerAlert.tsx b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/BannerAlert.tsx index b5787dfc25..561c38dfc1 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/BannerAlert.tsx +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/BannerAlert.tsx @@ -12,6 +12,7 @@ import {useBusinessRules} from './useBusinessRules'; // 2. Implement the `BannerAlert` component const BannerAlert = () => { // [END banner-alert.component] + // [START banner-alert.api] // 3. Setup the api const api = useApi<'pos.cash-session-details.banner.render'>(); @@ -34,12 +35,10 @@ const BannerAlert = () => { // [END banner-alert.loading-state] // [START banner-alert.render-implementation] - // 6. Display an alert banner if a business rule is violated + // 6. Display an alert banner when a business rule is violated if (isViolated) { return ; } - - return null; // [END banner-alert.render-implementation] }; diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/SafeModal.tsx b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/SafeModal.tsx index e33c5e775f..230d92f47c 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/SafeModal.tsx +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/SafeModal.tsx @@ -18,7 +18,7 @@ import { import type {Storage} from '@shopify/ui-extensions/point-of-sale'; // [START safe-modal.data-interfaces] -// 2. Define the data structures for safe activity data and safe details. +// 2. Define safe management data interface SafeActivity { id: number; type: string; diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/useBusinessRules.ts b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/useBusinessRules.ts index 95c323f3b8..8aae4d5303 100644 --- a/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/useBusinessRules.ts +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/useBusinessRules.ts @@ -46,8 +46,38 @@ export const fetchDrawerAmount = async (pointOfSaleDeviceId: string) => { }; // [END use-business-rules.direct-api] +// [START use-business-rules.check-drawer-amount] +// 3. Check if the drawer amount is within the min/max threshold +export const checkDrawerAmount = async ( + pointOfSaleDeviceId: string, +): Promise => { + const drawerAmount = await fetchDrawerAmount(pointOfSaleDeviceId); + if (drawerAmount < MIN_CASH_THRESHOLD) { + return { + isViolated: true, + ruleType: 'drawer_amount', + message: `Drawer amount is $${ + MIN_CASH_THRESHOLD - drawerAmount + } below the minimum threshold`, + }; + } + if (drawerAmount > MAX_CASH_THRESHOLD) { + return { + isViolated: true, + ruleType: 'drawer_amount', + message: `Drawer amount is $${ + drawerAmount - MAX_CASH_THRESHOLD + } above the maximum threshold`, + }; + } + return { + isViolated: false, + }; +}; +// [END use-business-rules.check-drawer-amount] + // [START use-business-rules.hook] -// 3. Implement the useBusinessRules hook +// 4. Implement the useBusinessRules hook export const useBusinessRules = (pointOfSaleDeviceId: string) => { const [isViolated, setIsViolated] = useState(false); const [alertMessage, setAlertMessage] = useState(''); @@ -78,33 +108,3 @@ export const useBusinessRules = (pointOfSaleDeviceId: string) => { }; }; // [END use-business-rules.hook] - -// [START use-business-rules.check-drawer-amount] -// 4. Check if the drawer amount is within the min/max threshold -export const checkDrawerAmount = async ( - pointOfSaleDeviceId: string, -): Promise => { - const drawerAmount = await fetchDrawerAmount(pointOfSaleDeviceId); - if (drawerAmount < MIN_CASH_THRESHOLD) { - return { - isViolated: true, - ruleType: 'drawer_amount', - message: `Drawer amount is $${ - MIN_CASH_THRESHOLD - drawerAmount - } below the minimum threshold`, - }; - } - if (drawerAmount > MAX_CASH_THRESHOLD) { - return { - isViolated: true, - ruleType: 'drawer_amount', - message: `Drawer amount is $${ - drawerAmount - MAX_CASH_THRESHOLD - } above the maximum threshold`, - }; - } - return { - isViolated: false, - }; -}; -// [END use-business-rules.check-drawer-amount] From 8c1d79fdcc80d2476b3f7c8924ba3b7939e95c43 Mon Sep 17 00:00:00 2001 From: Larris Xie Date: Wed, 23 Jul 2025 13:34:30 -0400 Subject: [PATCH 5/5] Adds InStoreCashInfo.tsx --- .../InStoreCashInfo.tsx | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/InStoreCashInfo.tsx diff --git a/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/InStoreCashInfo.tsx b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/InStoreCashInfo.tsx new file mode 100644 index 0000000000..3ad74a0051 --- /dev/null +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/InStoreCashInfo.tsx @@ -0,0 +1,102 @@ +import React, {useState, useEffect} from 'react'; + +import { + Text, + useApi, + reactExtension, +} from '@shopify/ui-extensions-react/point-of-sale'; + +// [START in-store-cash-info.component] +// 2. Implement the `InStoreCashInfo` component +const InStoreCashInfo = () => { + // [END in-store-cash-info.component] + + // [START in-store-cash-info.api] + // 3. Setup the api + const api = useApi<'pos.cash-session-details.block.render'>(); + const {locationId} = api.session.currentSession; + // [END in-store-cash-info.api] + + // [START in-store-cash-info.fetch-drawer-amount] + // 4. Fetch the total amount of cash on hand at the location + const [totalDrawerAmount, setTotalDrawerAmount] = useState( + null, + ); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const fetchTotalDrawerAmount = async (locationId: number) => { + const result = await fetch('shopify:admin/api/graphql.json', { + method: 'POST', + body: JSON.stringify({ + query: ` + TBD + `, + variables: { + locationId: `gid://shopify/Location/${locationId}`, + }, + }), + }); + + const json = await result.json(); + + if (json.errors) { + console.error('GraphQL Errors:', json.errors); + json.errors.forEach((error: any) => { + console.error('GraphQL Error Details:', error); + }); + return null; + } + + if (!result.ok) { + console.error('Network Error:', result.statusText); + return null; + } + + return json.data; + }; + + useEffect(() => { + const loadData = async () => { + try { + const amount = await fetchTotalDrawerAmount(locationId); + setTotalDrawerAmount(amount); + } catch (err) { + console.error('Error fetching drawer amount:', err); + setError('Unable to fetch cash drawer information'); + } + setLoading(false); + }; + + loadData(); + }, []); + // [END in-store-cash-info.fetch-drawer-amount] + + // [START in-store-cash-info.loading-error] + // 5. Handle loading and error states + if (loading) { + return Loading...; + } + + if (error) { + return {error}; + } + // [END in-store-cash-info.loading-error] + + // [START in-store-cash-info.render-implementation] + // 6. Display the total amount of cash on hand at the location + return ( + + {totalDrawerAmount !== null + ? `$${totalDrawerAmount.toFixed(2)}` + : 'No data available'} + + ); + // [END in-store-cash-info.render-implementation] +}; + +// [START in-store-cash-info.render-extension] +// 1. Render the InStoreCashInfo component at the `pos.cash-session-details.block.render` target +export default reactExtension('pos.cash-session-details.block.render', () => ( + +)); +// [END in-store-cash-info.render-extension]