@blocklet/payment-react
Version:
Reusable react components for payment kit v2
905 lines (778 loc) โข 25.8 kB
Markdown
# /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 /payment-react
```
## Quick Start
### Basic Integration
```tsx
import { PaymentProvider, CheckoutForm } from '/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 '/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 '/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 '/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 '/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 '/payment-react';
const translator = createTranslator({
en: {
checkout: { title: 'Complete Payment' }
},
zh: {
checkout: { title: 'ๅฎๆๆฏไป' }
}
});
// use payment-react locales
import { translations as extraTranslations } from '/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 '/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 '/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 '/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 '/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 '/payment-react';
import { useState, useEffect } from 'react';
import { Grid, Card, CardContent, Typography, Button } from '/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 '/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 '/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 '/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