From 3de7e18bb4f768d61649ab6dc5f3d55660331d7d Mon Sep 17 00:00:00 2001 From: Adam Vastopa Date: Wed, 4 Jun 2025 07:27:13 -0400 Subject: [PATCH] Add new checkout extension target with payment api --- .../src/surfaces/point-of-sale/api.ts | 18 +- .../render/api/payment-api/README.md | 154 ++++++++++++++++++ .../render/api/payment-api/payment-api.ts | 112 +++++++++++++ .../payment-details-api.ts | 5 + .../src/surfaces/point-of-sale/targets.ts | 23 ++- .../surfaces/point-of-sale/types/payment.ts | 30 ++++ 6 files changed, 338 insertions(+), 4 deletions(-) create mode 100644 packages/ui-extensions/src/surfaces/point-of-sale/render/api/payment-api/README.md create mode 100644 packages/ui-extensions/src/surfaces/point-of-sale/render/api/payment-api/payment-api.ts create mode 100644 packages/ui-extensions/src/surfaces/point-of-sale/render/api/payment-details-api/payment-details-api.ts diff --git a/packages/ui-extensions/src/surfaces/point-of-sale/api.ts b/packages/ui-extensions/src/surfaces/point-of-sale/api.ts index dba99ecde3..2e383b40fd 100644 --- a/packages/ui-extensions/src/surfaces/point-of-sale/api.ts +++ b/packages/ui-extensions/src/surfaces/point-of-sale/api.ts @@ -44,6 +44,17 @@ export type { export type {OrderApiContent, OrderApi} from './render/api/order-api/order-api'; +export type {PaymentDetailsApi} from './render/api/payment-details-api/payment-details-api'; + +export type { + PaymentApi, + PaymentApiContent, + PaymentAttemptResult, + PaymentAttemptOptions, + PaymentAttemptStatus, + PaymentResultCallback, +} from './render/api/payment-api/payment-api'; + export type { ProductApi, ProductApiContent, @@ -107,7 +118,12 @@ export type { export type {TaxLine} from './types/tax-line'; -export type {PaymentMethod, Payment} from './types/payment'; +export type { + PaymentMethod, + Payment, + PaymentTerminal, + PaymentWithDetails, +} from './types/payment'; export type {MultipleResourceResult} from './types/multiple-resource-result'; diff --git a/packages/ui-extensions/src/surfaces/point-of-sale/render/api/payment-api/README.md b/packages/ui-extensions/src/surfaces/point-of-sale/render/api/payment-api/README.md new file mode 100644 index 0000000000..46c29cf4ce --- /dev/null +++ b/packages/ui-extensions/src/surfaces/point-of-sale/render/api/payment-api/README.md @@ -0,0 +1,154 @@ +# Payment API + +The Payment API provides functionality for handling payment attempts and notifications in point-of-sale extensions. + +## Features + +- **Payment Processing**: Initiate payment attempts with callbacks for results +- **Status Tracking**: Monitor payment attempt status in real-time +- **Terminal Management**: Discover and manage payment terminals +- **Result Notifications**: Notify about payment results from external processors + +## Usage + +### Basic Payment Attempt + +```typescript +import type { + PaymentApi, + PaymentAttemptOptions, +} from '@shopify/ui-extensions/point-of-sale'; + +export default function PaymentExtension(root, {payment}: PaymentApi) { + async function processPayment() { + const options: PaymentAttemptOptions = { + payment: { + amount: 1500, // $15.00 in cents + currency: 'USD', + type: 'CreditCard', + description: 'Product purchase', + }, + timeout: 30000, // 30 seconds + requireConfirmation: true, + }; + + const attemptId = await payment.attemptPayment(options, (result) => { + console.log('Payment result:', result); + + if (result.status === 'completed') { + console.log( + 'Payment successful!', + result.details?.transactionReference, + ); + } else if (result.status === 'failed') { + console.error('Payment failed:', result.details?.errorMessage); + } + }); + + console.log('Payment attempt initiated:', attemptId); + } +} +``` + +### Terminal Management + +```typescript +export default function TerminalExtension(root, {payment}: PaymentApi) { + async function setupTerminal() { + // Get available terminals + const terminals = await payment.getAvailableTerminals(); + console.log('Available terminals:', terminals); + + // Set preferred terminal + if (terminals.length > 0) { + await payment.setPreferredTerminal(terminals[0].id); + console.log('Preferred terminal set:', terminals[0].name); + } + } +} +``` + +### Payment Status Monitoring + +```typescript +export default function MonitoringExtension(root, {payment}: PaymentApi) { + async function monitorPayment(attemptId: string) { + const status = await payment.getPaymentAttemptStatus(attemptId); + console.log('Current payment status:', status); + + // Cancel if needed + if (status === 'pending') { + await payment.cancelPaymentAttempt(attemptId); + console.log('Payment cancelled'); + } + } +} +``` + +### External Payment Processor Integration + +```typescript +export default function ExternalProcessorExtension( + root, + {payment}: PaymentApi, +) { + function handleExternalPaymentResult(externalResult: any) { + // Notify the POS system about payment result from external processor + payment.notifyPaymentResult({ + attemptId: externalResult.attemptId, + payment: { + amount: externalResult.amount, + currency: externalResult.currency, + type: 'StripeCreditCard', + terminal: { + id: externalResult.terminalId, + name: 'Stripe Terminal', + type: 'stripe_terminal', + isOnline: true, + }, + }, + status: externalResult.success ? 'completed' : 'failed', + details: { + transactionReference: externalResult.transactionId, + errorMessage: externalResult.errorMessage, + metadata: externalResult.metadata, + }, + completedAt: new Date(), + }); + } +} +``` + +## Types + +### PaymentAttemptStatus + +```typescript +type PaymentAttemptStatus = + | 'pending' // Payment is waiting to be processed + | 'processing' // Payment is currently being processed + | 'completed' // Payment completed successfully + | 'failed' // Payment failed + | 'cancelled' // Payment was cancelled + | 'timeout'; // Payment timed out +``` + +### PaymentAttemptResult + +Contains the complete result of a payment attempt including status, payment details, and any error information. + +### PaymentWithDetails + +Extended payment information that includes terminal details, tip amounts, and customer-facing descriptions. + +### PaymentTerminal + +Information about payment terminals including their ID, name, type, and online status. + +## Best Practices + +1. **Error Handling**: Always handle payment failures gracefully and provide clear error messages +2. **Timeouts**: Set appropriate timeout values based on payment method and expected processing time +3. **Terminal Selection**: Check terminal availability before attempting payments +4. **Status Monitoring**: Monitor payment status for long-running transactions +5. **Security**: Never log sensitive payment information in production diff --git a/packages/ui-extensions/src/surfaces/point-of-sale/render/api/payment-api/payment-api.ts b/packages/ui-extensions/src/surfaces/point-of-sale/render/api/payment-api/payment-api.ts new file mode 100644 index 0000000000..5c34a1046a --- /dev/null +++ b/packages/ui-extensions/src/surfaces/point-of-sale/render/api/payment-api/payment-api.ts @@ -0,0 +1,112 @@ +import type {PaymentWithDetails, PaymentTerminal} from '../../../types/payment'; + +/** + * Represents the status of a payment attempt + */ +export type PaymentAttemptStatus = + | 'pending' + | 'processing' + | 'completed' + | 'failed' + | 'cancelled' + | 'timeout'; + +/** + * Represents the result of a payment attempt + */ +export interface PaymentAttemptResult { + /** Unique identifier for the payment attempt */ + attemptId: string; + /** The payment information */ + payment: PaymentWithDetails; + /** The status of the payment attempt */ + status: PaymentAttemptStatus; + /** Additional details about the payment result */ + details?: { + /** Error message if the payment failed */ + errorMessage?: string; + /** Error code if the payment failed */ + errorCode?: string; + /** Transaction reference from the payment processor */ + transactionReference?: string; + /** Additional metadata from the payment processor */ + metadata?: Record; + }; + /** Timestamp when the payment attempt was completed */ + completedAt: Date; +} + +/** + * Options for initiating a payment attempt + */ +export interface PaymentAttemptOptions { + /** The payment information */ + payment: PaymentWithDetails; + /** Optional metadata to associate with the payment attempt */ + metadata?: Record; + /** Timeout in milliseconds for the payment attempt (default: 30000) */ + timeout?: number; + /** Whether to require confirmation before processing */ + requireConfirmation?: boolean; +} + +/** + * Callback function for payment attempt results + */ +export type PaymentResultCallback = (result: PaymentAttemptResult) => void; + +export interface PaymentApiContent { + /** + * Initiates a payment attempt and notifies the result via callback. + * @param options Payment attempt options including payment details + * @param callback Callback function to receive the payment result + * @returns A promise that resolves with the attempt ID + */ + attemptPayment: ( + options: PaymentAttemptOptions, + callback: PaymentResultCallback, + ) => Promise; + + /** + * Cancels a pending payment attempt. + * @param attemptId The ID of the payment attempt to cancel + * @returns A promise that resolves when the cancellation is processed + */ + cancelPaymentAttempt: (attemptId: string) => Promise; + + /** + * Gets the current status of a payment attempt. + * @param attemptId The ID of the payment attempt + * @returns A promise that resolves with the current status + */ + getPaymentAttemptStatus: ( + attemptId: string, + ) => Promise; + + /** + * Notifies about a payment attempt result. This is typically used + * when integrating with external payment processors. + * @param result The payment attempt result to notify + */ + notifyPaymentResult: (result: PaymentAttemptResult) => void; + + /** + * Gets a list of available payment terminals. + * @returns A promise that resolves with available terminals + */ + getAvailableTerminals: () => Promise; + + /** + * Sets the preferred terminal for payment processing. + * @param terminalId The ID of the terminal to use + * @returns A promise that resolves when the terminal is set + */ + setPreferredTerminal: (terminalId: string) => Promise; +} + +/** + * Payment API for handling payment attempts and notifications + */ +export interface PaymentApi { + payment: PaymentApiContent; +} diff --git a/packages/ui-extensions/src/surfaces/point-of-sale/render/api/payment-details-api/payment-details-api.ts b/packages/ui-extensions/src/surfaces/point-of-sale/render/api/payment-details-api/payment-details-api.ts new file mode 100644 index 0000000000..8153727f11 --- /dev/null +++ b/packages/ui-extensions/src/surfaces/point-of-sale/render/api/payment-details-api/payment-details-api.ts @@ -0,0 +1,5 @@ +import {Payment} from '../../../types/payment'; + +export interface PaymentDetailsApi { + paymentDetails: Payment; +} diff --git a/packages/ui-extensions/src/surfaces/point-of-sale/targets.ts b/packages/ui-extensions/src/surfaces/point-of-sale/targets.ts index 0fb9ff6936..077ba418eb 100644 --- a/packages/ui-extensions/src/surfaces/point-of-sale/targets.ts +++ b/packages/ui-extensions/src/surfaces/point-of-sale/targets.ts @@ -8,6 +8,8 @@ import type { DraftOrderApi, ProductApi, OrderApi, + PaymentDetailsApi, + PaymentApi, } from './api'; import type {RenderExtension} from './extension'; import type {Components} from './shared'; @@ -159,6 +161,21 @@ export interface ExtensionTargets { CartLineItemApi, BlockComponents >; + 'pos.checkout.payment-options.action.render': RenderExtension< + ActionTargetApi<'pos.checkout.payment-options.action.render'> & + CartApi & + PaymentDetailsApi & + PaymentApi, + BasicComponents + >; + 'pos.checkout.payment-options.action.menu-item.render': RenderExtension< + StandardApi<'pos.checkout.payment-options.action.menu-item.render'> & + ActionApi & + CartApi & + PaymentDetailsApi & + PaymentApi, + ActionComponents + >; 'pos.receipt-footer.block.render': RenderExtension< // NOTE: key/any type is cause of no arg useApi() that includes all target types. // stop using useApi() with no args, instead specify the target type explicitly. @@ -175,14 +192,14 @@ export type ExtensionForExtensionTarget = /** * For a given extension target, returns the value that is expected to be - * returned by that extension target’s callback type. + * returned by that extension target's callback type. */ export type ReturnTypeForExtension = ReturnType; /** * For a given extension target, returns the tuple of arguments that would - * be provided to that extension target’s callback type. + * be provided to that extension target's callback type. */ export type ArgumentsForExtension = Parameters; @@ -203,7 +220,7 @@ export type RenderExtensionTarget = { }[keyof ExtensionTargets]; /** - * A mapping of each “render extension” name to its callback type. + * A mapping of each "render extension" name to its callback type. */ export type RenderExtensions = { [ID in RenderExtensionTarget]: ExtensionTargets[ID]; diff --git a/packages/ui-extensions/src/surfaces/point-of-sale/types/payment.ts b/packages/ui-extensions/src/surfaces/point-of-sale/types/payment.ts index b851d8d4b5..42085ff3ae 100644 --- a/packages/ui-extensions/src/surfaces/point-of-sale/types/payment.ts +++ b/packages/ui-extensions/src/surfaces/point-of-sale/types/payment.ts @@ -14,3 +14,33 @@ export interface Payment { currency: string; type: PaymentMethod; } + +/** + * Information about the payment terminal or device used for processing + */ +export interface PaymentTerminal { + /** Unique identifier for the terminal */ + id: string; + /** Human-readable name of the terminal */ + name: string; + /** Type of terminal (e.g., 'stripe_terminal', 'square_terminal', etc.) */ + type: string; + /** Whether the terminal is currently online and available */ + isOnline: boolean; + /** Additional terminal-specific metadata */ + metadata?: Record; +} + +/** + * Extended payment information with additional processing details + */ +export interface PaymentWithDetails extends Payment { + /** Optional payment terminal information */ + terminal?: PaymentTerminal; + /** Optional tip amount */ + tip?: number; + /** Whether the payment supports partial payments */ + allowPartial?: boolean; + /** Customer-facing description of the payment */ + description?: string; +}