UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

905 lines (778 loc) โ€ข 25.8 kB
# @blocklet/payment-react [![npm version](https://img.shields.io/npm/v/@blocklet/payment-react)](https://www.npmjs.com/package/@blocklet/payment-react) A React component library for building payment flows, subscriptions, and donation systems in blocklets, seamlessly integrated with Payment Kit. ## Features - ๐Ÿ› ๏ธ **Pre-built UI Components**: Includes checkout forms, pricing tables, donation widgets, and more - ๐ŸŽจ **Customizable Themes**: Full control over styling via Material-UI themes - ๐ŸŒ **i18n Support**: Built-in localization for global audiences - ๐Ÿงฉ **Lazy Loading**: Optimize bundle size with dynamic imports - ๐Ÿ’ณ **Payment Operations**: Handle subscriptions, refunds, invoices, and metered billing ## Related Links - [Payment Kit Documentation](https://www.arcblock.io/docs/arcblock-payment-kit) - Official documentation with detailed guides and API references ## Installation ```bash npm install @blocklet/payment-react ``` ## Quick Start ### Basic Integration ```tsx import { PaymentProvider, CheckoutForm } from '@blocklet/payment-react'; function App() { return ( <PaymentProvider session={session} connect={connectApi}> <CheckoutForm id="plink_xxx" // Payment Link ID mode="inline" // Embed directly in your UI showCheckoutSummary={true} onChange={(state) => console.log('Checkout State:', state)} /> </PaymentProvider> ); } ``` ## Available Components & Utilities ### Core Components - `CheckoutForm` - Payment form for checkout sessions and payment links - `CheckoutTable` - Pricing table display - `CheckoutDonate` - Donation widget - `OverdueInvoicePayment` - Handle overdue invoice payments - `AutoTopup` - Auto-recharge configuration display card - `AutoTopupModal` - Auto-recharge configuration modal ### Form Components - `FormInput` - Base form input component - `PhoneInput` - Phone number input with validation - `AddressForm` - Complete address form - `StripeForm` - Stripe payment form - `CurrencySelector` - Currency selection dropdown - `CountrySelect` - Country selection dropdown ### Display Components - `Status` - Status indicator - `Livemode` - Test mode indicator - `Switch` - Toggle switch - `ConfirmDialog` - Confirmation dialog - `Amount` - Amount display with formatting - `TruncatedText` - Text truncation - `Link` - Safe navigation link ### UI Components ```tsx // Loading Button with state management import { LoadingButton } from '@blocklet/payment-react'; function PaymentButton() { const [loading, setLoading] = useState(false); const handlePayment = async () => { setLoading(true); try { await processPayment(); } finally { setLoading(false); } }; return ( <LoadingButton loading={loading} onClick={handlePayment} variant="contained" color="primary" > Pay Now </LoadingButton> ); } ``` ### Transaction Components - `TxLink` - Transaction link - `TxGas` - Gas fee display - `PaymentBeneficiaries` - Payment beneficiaries list ### History Components - `CustomerInvoiceList` - Invoice history list - `CustomerPaymentList` - Payment history list ### Context Providers - `PaymentProvider` - Payment context provider - `DonateProvider` - Donation context provider - `PaymentThemeProvider` - Theme provider ### Hooks - `useSubscription` - event socket callback - `useMobile` - Mobile detection ### Utilities #### API Client ```tsx import { api } from '@blocklet/payment-react'; // Basic usage const response = await api.get('/api/payments'); const data = await api.post('/api/checkout', { amount: 100 }); // With query parameters const results = await api.get('/api/invoices', { params: { status: 'paid' } }); // With request config const config = { headers: { 'Custom-Header': 'value' } }; const response = await api.put('/api/subscription', data, config); ``` #### Cached Request ```tsx import { CachedRequest } from '@blocklet/payment-react'; // Create a cached request const priceRequest = new CachedRequest( 'product-prices', () => api.get('/api/prices'), { strategy: 'session', // 'session' | 'local' | 'memory' ttl: 5 * 60 * 1000 // Cache for 5 minutes } ); // Use the cached request async function fetchPrices() { // Will use cache if available and not expired const prices = await priceRequest.fetch(); // Force refresh cache const freshPrices = await priceRequest.fetch(true); return prices; } ``` #### Date Handling ```tsx import { dayjs } from '@blocklet/payment-react'; // Format dates const formatted = dayjs().format('YYYY-MM-DD'); // Parse timestamps const date = dayjs(timestamp); const unix = date.unix(); // Relative time const relative = dayjs().from(date); ``` #### i18n Setup ```tsx // use your own translator import { createTranslator } from '@blocklet/payment-react'; const translator = createTranslator({ en: { checkout: { title: 'Complete Payment' } }, zh: { checkout: { title: 'ๅฎŒๆˆๆ”ฏไป˜' } } }); // use payment-react locales import { translations as extraTranslations } from '@blocklet/payment-react'; import merge from 'lodash/merge'; import en from './en'; import zh from './zh'; export const translations = merge( { zh, en, }, extraTranslations ); ``` #### Lazy Loading ```tsx import { createLazyComponent } from '@blocklet/payment-react'; const LazyComponent = createLazyComponent(async () => { const [{ Component }, { useHook }] = await Promise.all([ import('./Component'), import('./hooks') ]); globalThis.__DEPENDENCIES__ = { useHook }; return Component; }); ``` ### Auto-Topup Components The auto-topup feature allows users to automatically recharge their credit balance when it falls below a specified threshold. This helps ensure uninterrupted service usage. #### AutoTopup Display and manage auto-recharge configurations with different rendering modes. ```tsx import { AutoTopup, PaymentProvider } from '@blocklet/payment-react'; function CreditManagementPage() { return ( <PaymentProvider session={session}> {/* Default mode - fully expanded display */} <AutoTopup currencyId="credit-currency-id" onConfigChange={(config) => { console.log('Auto-topup config updated:', config); }} /> {/* Simple mode - collapsed by default, expandable */} <AutoTopup currencyId="credit-currency-id" mode="simple" onConfigChange={(config) => { // Handle configuration changes refreshCreditBalance(); }} /> {/* Custom mode - full control over rendering */} <AutoTopup currencyId="credit-currency-id" mode="custom" onConfigChange={(config) => console.log('Config updated:', config)} > {(openModal, config, paymentData, loading) => ( <div> {loading ? ( <div>Loading auto-topup configuration...</div> ) : ( <div> <h3>Auto Recharge Status</h3> <p>Status: {config?.enabled ? 'Active' : 'Inactive'}</p> {config?.enabled && ( <p> Threshold: {config.threshold} {config.currency?.symbol} </p> )} {paymentData?.balanceInfo && ( <p> Wallet Balance: {paymentData.balanceInfo.token} {config?.rechargeCurrency?.symbol} </p> )} <button onClick={openModal}>Configure Auto-Topup</button> </div> )} </div> )} </AutoTopup> </PaymentProvider> ); } ``` #### AutoTopupModal Configure auto-recharge settings including threshold, payment method, and purchase amount. ```tsx import { AutoTopupModal, PaymentProvider } from '@blocklet/payment-react'; import { useState } from 'react'; function AutoRechargeSettings({ currencyId }) { const [modalOpen, setModalOpen] = useState(false); const handleSuccess = (config) => { console.log('Auto-recharge configured:', config); setModalOpen(false); // Refresh the parent component or update state }; const handleError = (error) => { console.error('Configuration failed:', error); }; return ( <PaymentProvider session={session}> <button onClick={() => setModalOpen(true)}> Setup Auto-Recharge </button> <AutoTopupModal open={modalOpen} onClose={() => setModalOpen(false)} currencyId={currencyId} onSuccess={handleSuccess} onError={handleError} defaultEnabled={true} // Start with auto-recharge enabled /> </PaymentProvider> ); } ``` #### Component Props **AutoTopup Props:** - `currencyId` [Required] - The currency ID for auto-recharge - `onConfigChange` [Optional] - Callback when configuration changes: `(config: AutoRechargeConfig) => void` - `mode` [Optional] - Rendering mode: `'default' | 'simple' | 'custom'` - `sx` [Optional] - Custom styles - `children` [Optional] - Custom render function for `custom` mode: `(openModal, config, paymentData, loading) => ReactNode` **AutoTopupModal Props:** - `open` [Required] - Whether modal is open - `onClose` [Required] - Close modal callback - `currencyId` [Required] - The currency ID for auto-recharge - `customerId` [Optional] - Customer ID (defaults to current session user) - `onSuccess` [Optional] - Success callback: `(config: AutoRechargeConfig) => void` - `onError` [Optional] - Error callback: `(error: any) => void` - `defaultEnabled` [Optional] - Whether to default the enabled state to true ## Complete Examples ### Donation Page Example ```tsx import { DonateProvider, CheckoutDonate, PaymentProvider } from '@blocklet/payment-react'; import { useEffect, useState } from 'react'; function DonationPage() { const [session, setSession] = useState(null); useEffect(() => { // Get session from your auth system const getSession = async () => { const userSession = await fetchSession(); setSession(userSession); }; getSession(); }, []); return ( <PaymentProvider session={session} connect={connectApi}> <DonateProvider mountLocation="your-unique-donate-instance" description="Help locate this donation instance" defaultSettings={{ btnText: 'Like', }} > <CheckoutDonate settings={{ target: "post-123", // required, unique identifier for the donation instance title: "Support Author", // required, title of the donation modal description: "If you find this article helpful, feel free to buy me a coffee", // required, description of the donation reference: "https://your-site.com/posts/123", // required, reference link of the donation beneficiaries: [ { address: "tip user did", // required, address of the beneficiary share: "100", // required, percentage share }, ], }} /> {/* Custom donation history display */} <CheckoutDonate mode="custom" settings={{ target: "post-123", // required, unique identifier for the donation instance title: "Support Author", // required, title of the donation modal description: "If you find this article helpful, feel free to buy me a coffee", // required, description of the donation reference: "https://your-site.com/posts/123", // required, reference link of the donation beneficiaries: [ { address: "tip user did", // required, address of the beneficiary share: "100", // required, percentage share }, ], }} > {(openDonate, totalAmount, supporters, loading, settings) => ( <div> <h2>Our Supporters</h2> {loading ? ( <CircularProgress /> ) : ( <div> <div> Total Donations: {totalAmount} {supporters.currency?.symbol} </div> <div> {supporters.supporters.map(supporter => ( <div key={supporter.id}> <span>{supporter.customer?.name}</span> <span>{supporter.amount_total} {supporters.currency?.symbol}</span> </div> ))} </div> </div> )} </div> )} </CheckoutDonate> </DonateProvider> </PaymentProvider> ); } ``` ### Auto-Topup Integration Example Complete example showing how to integrate auto-topup functionality into a credit management dashboard. ```tsx import { PaymentProvider, AutoTopup, AutoTopupModal, CustomerInvoiceList, Amount } from '@blocklet/payment-react'; import { useState, useEffect } from 'react'; import { Grid, Card, CardContent, Typography, Button } from '@mui/material'; function CreditDashboard() { const [session, setSession] = useState(null); const [creditCurrencies, setCreditCurrencies] = useState([]); const [showSetupModal, setShowSetupModal] = useState(false); const [selectedCurrency, setSelectedCurrency] = useState(null); useEffect(() => { // Initialize session and fetch credit currencies const initializeDashboard = async () => { const userSession = await fetchSession(); setSession(userSession); const currencies = await fetchCreditCurrencies(); setCreditCurrencies(currencies); }; initializeDashboard(); }, []); const handleAutoTopupChange = (currencyId, config) => { console.log(`Auto-topup updated for ${currencyId}:`, config); // Update local state or refetch data }; return ( <PaymentProvider session={session}> <Grid container spacing={3}> {/* Credit Balance Cards with Auto-Topup */} {creditCurrencies.map((currency) => ( <Grid item xs={12} md={6} key={currency.id}> <Card> <CardContent> <Typography variant="h6" gutterBottom> {currency.name} Balance </Typography> {/* Current balance display */} <Typography variant="h4" color="primary" gutterBottom> <Amount amount={currency.balance} decimal={currency.decimal} symbol={currency.symbol} /> </Typography> {/* Auto-topup configuration */} <AutoTopup currencyId={currency.id} mode="simple" onConfigChange={(config) => handleAutoTopupChange(currency.id, config) } sx={{ mt: 2 }} /> {/* Manual setup button for currencies without auto-topup */} <Button variant="outlined" onClick={() => { setSelectedCurrency(currency); setShowSetupModal(true); }} sx={{ mt: 1 }} > Configure Auto-Recharge </Button> </CardContent> </Card> </Grid> ))} {/* Usage History */} <Grid item xs={12}> <Card> <CardContent> <Typography variant="h6" gutterBottom> Credit Usage History </Typography> <CustomerInvoiceList customer_id={session?.user?.did} type="table" include_staking status="paid,open" /> </CardContent> </Card> </Grid> </Grid> {/* Auto-topup Setup Modal */} {showSetupModal && selectedCurrency && ( <AutoTopupModal open={showSetupModal} onClose={() => { setShowSetupModal(false); setSelectedCurrency(null); }} currencyId={selectedCurrency.id} onSuccess={(config) => { console.log('Auto-topup configured:', config); handleAutoTopupChange(selectedCurrency.id, config); setShowSetupModal(false); setSelectedCurrency(null); }} onError={(error) => { console.error('Auto-topup setup failed:', error); }} defaultEnabled={true} /> )} </PaymentProvider> ); } // Custom auto-topup display using custom mode function CustomAutoTopupDisplay({ currencyId }) { return ( <AutoTopup currencyId={currencyId} mode="custom" > {(openModal, config, paymentData, loading) => { if (loading) return <div>Loading...</div>; return ( <Card variant="outlined"> <CardContent> <Typography variant="subtitle1" gutterBottom> Smart Auto-Recharge </Typography> {config?.enabled ? ( <div> <Typography color="success.main" gutterBottom> โœ“ Active - Recharges when balance drops below {config.threshold} {config.currency?.symbol} </Typography> <Typography variant="body2" color="text.secondary"> Next recharge: {config.quantity}x {config.price?.product?.name} </Typography> {paymentData?.balanceInfo && ( <Typography variant="body2" sx={{ mt: 1 }}> Wallet Balance: {paymentData.balanceInfo.token} {config.rechargeCurrency?.symbol} </Typography> )} </div> ) : ( <Typography color="text.secondary" gutterBottom> Auto-recharge is not configured </Typography> )} <Button variant="contained" size="small" onClick={openModal} sx={{ mt: 2 }} > {config?.enabled ? 'Modify Settings' : 'Setup Auto-Recharge'} </Button> </CardContent> </Card> ); }} </AutoTopup> ); } ``` ### Subscription Management Example - `ResumeSubscription` component - Resume subscription, with support for re-stake if needed - Props: - `subscriptionId`: [Required] The subscription ID to resume - `onResumed`: [Optional] Callback function called after successful resume, receives `(subscription)` - `dialogProps`: [Optional] Dialog properties, default is `{ open: true }` - `successToast`: [Optional] Whether to show success toast, default is `true` - `authToken`: [Optional] Authentication token for API requests ```tsx import { PaymentProvider, ResumeSubscription, CustomerInvoiceList, Amount } from '@blocklet/payment-react'; function SubscriptionPage({ subscriptionId }) { return ( <PaymentProvider session={session}> <ResumeSubscription subscriptionId={subscriptionId} onResumed={(subscription) => { // Refresh subscription status refetchSubscription(); }} /> {/* Custom dialog props */} <ResumeSubscription subscriptionId={subscriptionId} dialogProps={{ open: true, title: 'Resume Your Subscription', onClose: () => { // Handle dialog close } }} /> {/* With auth token */} <ResumeSubscription subscriptionId={subscriptionId} authToken="your-auth-token" /> </PaymentProvider> ); } ``` - `OverdueInvoicePayment` component - Display overdue invoices for a subscription, and support batch payment - Props: - `subscriptionId`: [Optional] The subscription ID - `customerId`: [Optional] The customer ID or DID - `onPaid`: [Optional] Callback function called after successful payment, receives `(id, currencyId, type)` - `mode`: [Optional] Component mode, `default` or `custom` (default is `default`) - `dialogProps`: [Optional] Dialog properties, default is `{ open: true }` - `detailLinkOptions`: [Optional] Detail link options, format: `{ enabled, onClick, title }` - `successToast`: [Optional] Whether to show success toast, default is `true` - `children`: [Optional] Custom rendering function, used only when `mode="custom"` - Custom Mode: - `children` function receives two parameters: - `handlePay`: Function to start the payment process - `data`: Payment data (includes `subscription`, `summary`, `invoices`, `subscriptionCount`, `detailUrl`) ```tsx import { PaymentProvider, OverdueInvoicePayment, CustomerInvoiceList, Amount } from '@blocklet/payment-react'; function SubscriptionPage({ subscriptionId }) { return ( <PaymentProvider session={session}> {/* Handle subscription overdue payments */} <OverdueInvoicePayment subscriptionId={subscriptionId} onPaid={() => { // Refresh subscription status refetchSubscription(); }} /> {/* Handle customer overdue payment */} <OverdueInvoicePayment customerId={session.user.did} onPaid={() => { // Refresh customer status refetch(); }} /> {/* Custom Overdue Invoice Payment */} <OverdueInvoicePayment subscriptionId={subscriptionId} onPaid={() => { refetchSubscription(); }} mode="custom" > {(handlePay, { subscription, summary, invoices }) => ( <Card> <CardHeader title="Overdue Payments" /> <CardContent> <Stack spacing={2}> {Object.entries(summary).map(([currencyId, info]) => ( <div key={currencyId}> <Typography> Due Amount: <Amount amount={info.amount} /> {info.currency?.symbol} </Typography> <Button onClick={() => handlePay(info)} variant="contained" > Pay Now </Button> </div> ))} </Stack> </CardContent> </Card> )} </OverdueInvoicePayment> {/* Display invoice history */} <CustomerInvoiceList subscription_id={subscriptionId} type="table" include_staking status="open,paid,uncollectible,void" /> </PaymentProvider> ); } ``` ## Best Practices ### Cache Management ```tsx // 1. Choose appropriate cache strategy const shortLivedCache = new CachedRequest('key', fetchData, { strategy: 'memory', ttl: 60 * 1000 // 1 minute }); const persistentCache = new CachedRequest('key', fetchData, { strategy: 'local', ttl: 24 * 60 * 60 * 1000 // 1 day }); // 2. Clear cache when data changes async function updateData() { await api.post('/api/data', newData); await cache.fetch(true); // Force refresh } // 3. Handle cache errors try { const data = await cache.fetch(); } catch (err) { console.error('Cache error:', err); // Fallback to fresh data const fresh = await cache.fetch(true); } ``` ### Bundle Optimization - Use lazy loading for non-critical components - Import only required components - Leverage code splitting with dynamic imports ### Theme Consistency - Maintain consistent styling across components - Use theme provider for global style changes - Override styles at component level when needed ### Theme Customization Since version 1.14.22, the component includes a built-in theme provider. If you need to modify the styles of internal components, pass the `theme` property to override or inherit the external theme. | Option | Description | | --- | --- | | default | Wrapped with built-in `PaymentThemeProvider` | | inherit | Use the parent component's themeProvider | | PaymentThemeOptions | Override some styles of `PaymentThemeProvider` | ```tsx // 1. Use themeOptions <CheckoutForm id="plink_xxx" onChange={console.info} theme={{ components: { MuiButton: { styleOverrides: { containedPrimary: { backgroundColor: '#1DC1C7', color: '#fff', '&:hover': { backgroundColor: 'rgb(20, 135, 139)', }, }, }, }, }, }} /> // 2. Use theme sx <CheckoutForm id="plink_xxx" showCheckoutSummary={false} onChange={console.info} theme={{ sx: { '.cko-submit-button': { backgroundColor: '#1DC1C7', color: '#fff', '&:hover': { backgroundColor: 'rgb(20, 135, 139)', }, }, }, }} /> ``` ### Status & Utility Components ```tsx import { Status, Livemode, Switch, Link, Amount } from '@blocklet/payment-react'; // Status indicator for payment states <Status label="active" color="success" size="small" sx={{ margin: 1 }} /> // Test mode indicator <Livemode /> // Custom switch button <Switch checked={true} onChange={(checked) => console.log('Switched:', checked)} /> // Safe navigation link <Link to="/demo" /> ## License Apache-2.0