# @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

