UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

593 lines (506 loc) โ€ข 16 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 Blocklet applications. Seamlessly integrated with Blocklet's payment infrastructure. ## 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/en/start-payment-react) - Official documentation with detailed guides and API references - [Example Implementation](https://github.com/blocklet/payment-kit/blob/master/blocklets/example/src/pages/checkout.jsx) - Complete example showing how to integrate the payment components ## 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 ### 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; }); ``` ## 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> ); } ``` ### 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