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..561c38dfc1 --- /dev/null +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/BannerAlert.tsx @@ -0,0 +1,50 @@ +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 when a business rule is violated + if (isViolated) { + return ; + } + // [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/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] 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..230d92f47c --- /dev/null +++ b/packages/ui-extensions/docs/surfaces/point-of-sale/mdxExamples/cash-management-example/SafeModal.tsx @@ -0,0 +1,257 @@ +import React, {useState, useEffect} from 'react'; + +import { + Text, + Screen, + ScrollView, + reactExtension, + useApi, + Button, + Stack, + 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 safe management data +interface SafeActivity { + id: number; + type: string; + amount: number; + timestamp: Date; + staffName?: string; +} + +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(''); + const [balance, setBalance] = useState(0); + const [activities, setActivities] = useState([]); + // [END safe-modal.component] + + // [START safe-modal.api-storage] + // 4. Setup the api and storage + const api = useApi<'pos.cash-session-details.action.render'>(); + const storage: Storage = api.storage; + const {staffMemberId} = api.session.currentSession; + // [END safe-modal.api-storage] + + // [START safe-modal.load-data] + // 5. Intalize or 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] + // 6. Implement deposit and withdraw logic + const handleActivity = async () => { + const activityAmount = parseFloat(amount); + + try { + if (activityType === 'deposit') { + setBalance(balance + activityAmount); + } else if (activityType === 'withdraw') { + setBalance(balance - activityAmount); + } + await storage.set('balance', balance); + + const newActivity: SafeActivity = { + id: activities.length + 1, + type: activityType, + amount: activityAmount, + timestamp: new Date(), + staffName: staffMemberId?.toString(), + }; + setActivities([...activities, newActivity]); + await storage.set('activities', activities); + + setAmount(''); + api.navigation.pop(); + } catch (error) { + console.error('Error saving activity:', error); + } + }; + // [END safe-modal.handle-activity] + + // [START safe-modal.validation] + // 7. Check if the activity amount is valid + const canSubmit = () => { + const activityAmount = parseFloat(amount); + if (isNaN(activityAmount) || activityAmount <= 0) return false; + if (activityType === 'withdraw' && activityAmount > balance) return false; + return true; + }; + // [END safe-modal.validation] + + // [START safe-modal.format-activities] + // 8. Format the activities into a list + const activityListData = (activities || []).map((activity: SafeActivity) => ({ + id: activity.id.toString(), + leftSide: { + label: new Date(activity.timestamp).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + }), + subtitle: [ + { + content: activity.staffName, + }, + ] as [ListRowSubtitle], + }, + rightSide: { + label: `${activity.type === 'deposit' ? '+' : '-'} $${( + activity.amount || 0 + ).toFixed(2)}`, + }, + })); + // [END safe-modal.format-activities] + + // [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 ( + + + + + + Manage Safe + + + + + + + + + + + + + + +