From be697ad438a068b46c41fd87d2e5257d91dd7dea Mon Sep 17 00:00:00 2001 From: Kristijan Date: Fri, 13 Feb 2026 14:04:22 +0100 Subject: [PATCH] Version 1.2.36 - 2026-02-13 14:04:22 --- package.json | 2 +- src/app/errors/page.mdx | 2 +- src/app/quick-start/page.mdx | 122 ++++++++++++++++++ src/app/sandbox/page.mdx | 31 ++++- src/app/webhooks/page.mdx | 224 +++++++++++++++++++++++++--------- src/components/Guides.tsx | 5 + src/components/Navigation.tsx | 1 + 7 files changed, 323 insertions(+), 64 deletions(-) create mode 100644 src/app/quick-start/page.mdx diff --git a/package.json b/package.json index 92e31db..cc35aa5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tailwind-plus-icpay", - "version": "1.2.35", + "version": "1.2.36", "private": true, "packageManager": "pnpm@9.12.3", "scripts": { diff --git a/src/app/errors/page.mdx b/src/app/errors/page.mdx index 99954df..58eb0e0 100644 --- a/src/app/errors/page.mdx +++ b/src/app/errors/page.mdx @@ -76,7 +76,7 @@ try {
-
diff --git a/src/app/quick-start/page.mdx b/src/app/quick-start/page.mdx new file mode 100644 index 0000000..1bc68f0 --- /dev/null +++ b/src/app/quick-start/page.mdx @@ -0,0 +1,122 @@ +export const metadata = { + title: 'Quick start guide', + description: + 'Create an ICPay account, get your API keys, and add a pay button to your site with the ICPay Widget and Next.js.', +} + +export const sections = [ + { title: 'Create an account', id: 'create-account' }, + { title: 'Get your API keys', id: 'api-keys' }, + { title: 'Add a pay button with Next.js', id: 'pay-button-nextjs' }, + { title: 'Try it in the demo', id: 'try-demo' }, +] + +# Quick start guide + +This guide walks you through creating an ICPay account, getting your publishable key, and adding a pay button to your site using Next.js. No prior crypto or blockchain experience required. {{ className: 'lead' }} + +## Create an account + +1. Go to **[icpay.org](https://icpay.org)** and click **Sign up** (or **Log in** if you already have an account). +2. Register with your email and set a password. You can also use social login if available. +3. After signing in, you’ll land in the **dashboard**. Here you can manage payments, view balances, and find your API keys. + +For testing without real funds, you can use the **sandbox** at [betterstripe.com](https://betterstripe.com). Accounts and keys there are separate from production—see the [Sandbox Server](/sandbox) docs. + +## Get your API keys + +To show a pay button on your site, you only need the **publishable key**. It’s safe to use in the browser. + +1. In the [icpay.org](https://icpay.org) dashboard, open **Settings** (or **API keys** / **Developers**, depending on the menu). +2. Find your **Publishable key**. It usually starts with `pk_` (for example, `pk_live_...` in production or `pk_test_...` in sandbox). +3. Copy the key. You’ll use it in your Next.js app in the next step. + +Keep your **Secret key** (if you see it) private and only use it on the server. For a simple pay button, the publishable key is enough. + +## Add a pay button with Next.js + +These steps add a single “Pay with crypto” button that opens the ICPay payment flow. The code matches what you can generate on [demo.icpay.org](https://demo.icpay.org) when you choose the Pay Button and the Next.js tab. + +### 1. Install the widget + +In your Next.js project folder, run: + +```bash +npm install @ic-pay/icpay-widget @ic-pay/icpay-sdk +``` + +(or `pnpm add` / `yarn add` if you use those). + +### 2. Add your publishable key to environment variables + +Create or edit `.env` in the project root and add: + +``` +NEXT_PUBLIC_ICPAY_PK=pk_your_publishable_key_here +``` + +Replace `pk_your_publishable_key_here` with the key you copied from the dashboard. The `NEXT_PUBLIC_` prefix makes it available in the browser, which is what the widget needs. + +### 3. Create a page with the pay button + +Create a new page (for example `app/checkout/page.tsx`) or add the snippet below to an existing page. This uses the **Pay Button** widget: one button that opens the payment modal for a fixed amount. + +```tsx +'use client' +import { useEffect, useRef } from 'react' +import '@ic-pay/icpay-widget' + +export default function Checkout() { + const elRef = useRef(null) + + useEffect(() => { + const el = elRef.current + if (!el) return + + el.config = { + publishableKey: process.env.NEXT_PUBLIC_ICPAY_PK, + amountUsd: 9.99, + buttonLabel: 'Pay $9.99 with crypto', + } + + const onSuccess = (e: any) => console.log('Payment completed', e.detail) + const onError = (e: any) => console.error('Payment error', e.detail) + + el.addEventListener('icpay-pay', onSuccess) + el.addEventListener('icpay-error', onError) + + return () => { + el.removeEventListener('icpay-pay', onSuccess) + el.removeEventListener('icpay-error', onError) + } + }, []) + + return +} +``` + +- **`publishableKey`**: Your key from the dashboard (from `.env`). +- **`amountUsd`**: The amount in USD (e.g. `9.99`). +- **`buttonLabel`**: Optional text on the button. + +When a visitor clicks the button, they’ll see the ICPay payment UI to choose a wallet and complete the payment. You can change `amountUsd` and `buttonLabel` to match your product or donation amount. + +### 4. Run your app + +Run your dev server (e.g. `npm run dev`) and open the page that renders ``. Click the button and complete a test payment. In production, use your live publishable key from [icpay.org](https://icpay.org); for testing, use the sandbox at [betterstripe.com](https://betterstripe.com) and a `pk_test_...` key. + +## Try it in the demo + +You can try the Pay Button (and other widgets) and get ready-made code without writing anything first: + +1. Go to **[demo.icpay.org](https://demo.icpay.org)**. +2. Select **Pay Button** (or another component like Tip Jar or Coffee Shop). +3. Enter your publishable key (or use the demo key to preview). +4. Adjust the amount, label, and other options. +5. Open the **Next.js** tab and copy the generated code into your project. + +The generated snippet is the same pattern as above: install the widget, set `el.config` with your `publishableKey` and options, and listen for `icpay-pay` and `icpay-error`. You can paste it into a new `app/checkout/page.tsx` (or similar) and add your key to `.env`. + +--- + +For more widget types (tip jar, premium content, donation thermometer) and options, see [Widget components](/widget) and [Pay Button](/widget/components/pay-button). For server-side operations and webhooks, see the [Private SDK](/sdk-secret) and [Webhooks](/webhooks). diff --git a/src/app/sandbox/page.mdx b/src/app/sandbox/page.mdx index c2decc7..9b17fd2 100644 --- a/src/app/sandbox/page.mdx +++ b/src/app/sandbox/page.mdx @@ -1,12 +1,13 @@ export const metadata = { title: 'Sandbox Server', description: - 'Information about the ICPay sandbox server for testing and development. Access the sandbox at betterstripe.com to test new features with free faucet tokens on Base Sepolia, SKALE Base Testnet, ARC Network Testnet, and Solana Devnet.', + 'Information about the ICPay sandbox server for testing and development. Access the sandbox at betterstripe.com, demo.betterstripe.com, and pos.betterstripe.com. Sandbox users and accounts are separate from live production icpay.org. Test with free faucet tokens on Base Sepolia, SKALE Base Testnet, ARC Network Testnet, and Solana Devnet.', } export const sections = [ { title: 'Overview', id: 'overview' }, { title: 'Test Networks', id: 'test-networks' }, + { title: 'Wallet setup for Solana Devnet', id: 'wallet-setup-solana-devnet' }, { title: 'Using with Widget', id: 'using-with-widget' }, ] @@ -20,12 +21,17 @@ The sandbox server is available at: - **Website**: [https://betterstripe.com](https://betterstripe.com) - **API**: [https://api.betterstripe.com](https://api.betterstripe.com) +- **Demo**: [https://demo.betterstripe.com](https://demo.betterstripe.com) +- **POS**: [https://pos.betterstripe.com](https://pos.betterstripe.com) + +[demo.betterstripe.com](https://demo.betterstripe.com) and [pos.betterstripe.com](https://pos.betterstripe.com) are linked to the sandbox environment and use the same test networks and sandbox API. ### Important Notes - **Sandbox Environment**: This is a testing environment and may be unavailable at times due to feature rollouts, updates, or maintenance. - **New Features First**: New features and updates are typically deployed to the sandbox before production, making it ideal for early testing and integration. - **Test Networks**: The sandbox includes mainnets but also testnet and devnet networks (Base Sepolia, SKALE Base Testnet, ARC Network Testnet, and Solana Devnet) so you can test with free faucet tokens without spending real funds. +- **Separate from production**: Users and accounts created in the sandbox (betterstripe.com, demo.betterstripe.com, pos.betterstripe.com) are **not** the same as users and accounts on live production [icpay.org](https://icpay.org). You must create separate accounts for sandbox testing and for production use. ## Test Networks @@ -49,6 +55,29 @@ All test networks provide free test tokens through their respective faucets: These test tokens have no real-world value and are intended solely for development and testing purposes. +## Wallet setup for Solana Devnet + +To test Solana payments on the sandbox with devnet tokens, use a Solana wallet in developer/testnet mode. Below is how to configure **Phantom** and **Backpack**. + +### Phantom + +1. Open Phantom and go to **Settings** (gear icon). +2. Open **Developer Settings** (or **Security & Privacy** → **Developer Settings** in some versions). +3. Turn on **Developer Mode** (or **Testnet Mode**) so you can switch the wallet to Solana Devnet. +4. In the network selector, choose **Devnet** so the wallet uses Solana Devnet and your devnet SOL/tokens. + +### Backpack + +1. Open Backpack and go to **Settings**. +2. Enable **Developer Mode** so you can use test networks and custom RPCs. +3. To test with devnet tokens, set the Solana RPC to the devnet endpoint: + - Go to **Settings** → **Networks** (or **Developer** / **RPC**). + - Set the Solana RPC URL to: `https://api.devnet.solana.com` + + Without this RPC setting, Backpack may use mainnet by default and devnet transactions (e.g. from the [Solana Faucet](https://faucet.solana.com)) will not appear or work correctly. + +After configuring your wallet, request test SOL from the [Solana Faucet](https://faucet.solana.com) or [QuickNode Faucet](https://faucet.quicknode.com/solana/devnet) and use the sandbox (betterstripe.com) to complete devnet payments. + ## Using with Widget To use the ICPay Widget with the sandbox server, configure the `apiUrl` option in your widget config to point to the sandbox API endpoint. diff --git a/src/app/webhooks/page.mdx b/src/app/webhooks/page.mdx index e9a35fc..6291289 100644 --- a/src/app/webhooks/page.mdx +++ b/src/app/webhooks/page.mdx @@ -9,6 +9,13 @@ export const sections = [ { title: 'Create endpoints', id: 'create-endpoints' }, { title: 'Event types', id: 'event-types' }, { title: 'Payload format', id: 'payload-format' }, + { title: 'payment_intent.created', id: 'event-payment-intent-created' }, + { title: 'payment.created', id: 'event-payment-created' }, + { title: 'payment.updated', id: 'event-payment-updated' }, + { title: 'payment.completed', id: 'event-payment-completed' }, + { title: 'payment.failed', id: 'event-payment-failed' }, + { title: 'payment.cancelled', id: 'event-payment-cancelled' }, + { title: 'payment.refunded', id: 'event-payment-refunded' }, { title: 'Verify signatures', id: 'verify-signatures' }, { title: 'Example handlers', id: 'example-handlers' }, { title: 'Best practices', id: 'best-practices' }, @@ -16,45 +23,40 @@ export const sections = [ # Webhooks -Webhooks let your backend react to ICPay events such as payments being created or completed. Endpoints receive signed JSON payloads with Stripe-like structure and `X-ICPay-Signature`. {{ className: 'lead' }} +Webhooks let your backend react to ICPay events such as payment intents being created or payments being completed. Endpoints receive signed JSON payloads with a Stripe-like structure and `X-ICPay-Signature`. {{ className: 'lead' }} ## Overview -- Configure endpoints in `icpay.org` under Settings → Webhook Endpoints. +- Configure endpoints in **icpay.org** under Settings → Webhook Endpoints. - Subscribe per-endpoint to one or more event types. -- ICPay retries failed deliveries up to your endpoint’s configured retry count. +- ICPay retries failed deliveries according to your endpoint’s retry count. ## Create endpoints -In `icpay.org` → Settings → Webhook Endpoints, click “New Endpoint”, set: +In **icpay.org** → Settings → Webhook Endpoints, click **New Endpoint** and set: -- URL: your HTTPS receiver. -- Subscribed events. -- Retry count and timeout. +- **URL**: Your HTTPS receiver. +- **Subscribed events**: One or more of the event types listed below. +- **Retry count** and **timeout**. - Optional custom headers. ## Event types -Supported event types map to `WebhookEventType` in the API: - -- payment.created -- payment.updated -- payment.completed -- payment.failed -- payment.cancelled -- payment.refunded -- payment_intent.created -- account.updated -- account.verified -- wallet.created -- wallet.updated -- wallet.balance_changed -- ledger.created -- ledger.updated +Only the following event types are currently emitted. Subscribe to these in your endpoint: + +| Event type | When it is triggered | +|------------|------------------------| +| `payment_intent.created` | A new payment intent is created (e.g. when a customer starts checkout). | +| `payment.created` | A payment record is created or first linked to a transaction. | +| `payment.updated` | A payment’s attributes change (e.g. status) without transitioning to completed/failed/cancelled/refunded. | +| `payment.completed` | A payment has been successfully completed. | +| `payment.failed` | A payment has failed. | +| `payment.cancelled` | A payment was cancelled. | +| `payment.refunded` | A payment was refunded. | ## Payload format -Deliveries use this canonical shape: +Every delivery uses this envelope. The `data.object` and optional `data.previous_attributes` depend on the event type. ```json { @@ -63,8 +65,8 @@ Deliveries use this canonical shape: "api_version": "2025-08-11", "created": 1733966400, "data": { - "object": { /* domain object, e.g. payment */ }, - "previous_attributes": { /* optional diff */ } + "object": { }, + "previous_attributes": { } }, "livemode": true, "pending_webhooks": 1, @@ -73,49 +75,147 @@ Deliveries use this canonical shape: } ``` -Example `payment.completed` (includes mismatch context when applicable). When a normal transaction has an amount mismatch, the payment status is `mismatched` and both amounts are present: +- **id**: Unique event id (e.g. `evt_...`). Use for idempotency. +- **type**: One of the event types above. +- **data.object**: The resource (payment intent or payment); see each event section below. +- **data.previous_attributes**: Present for `payment.updated` (and similar) when relevant; contains the previous values of changed fields. + +Sections below describe the **data.object** (and where applicable **data.previous_attributes**) for each event type. + +## payment_intent.created + +**Triggered when:** A new payment intent is created via the API (e.g. when a customer initiates checkout). + +**Emitted by:** ICPay API when `createPaymentIntent` succeeds. + +**data.object** (payment intent): ```json { - "type": "payment.completed", - "data": { - "object": { - "id": "pay_123", - "accountId": "acc_abc", - "paymentIntentId": "pi_123", - "transactionId": "tx_789", - "transactionSplitId": "spl_456", - "basePaymentAccountId": "acc_base", - "canisterTxId": 123456, - "amount": "150000000", - "ledgerCanisterId": "ryjl3-tyaaa-aaaaa-aaaba-cai", - "ledgerTxId": "ltx_001", - "accountCanisterId": 42, - "status": "mismatched", - "requestedAmount": "150000000", - "paidAmount": "140000000", // If the amounts match, `status` will be `completed`. - "invoiceId": "inv_001", - "metadata": {"orderId": "ORD-123"}, - "createdAt": "2025-01-01T12:00:00Z", - "updatedAt": "2025-01-01T12:00:10Z" - } - } + "id": "pi_...", + "accountId": "acc_...", + "amount": "100000000", + "ledgerCanisterId": "ryjl3-tyaaa-aaaaa-aaaba-cai", + "description": "Order #123", + "expectedSenderPrincipal": null, + "status": "requires_payment", + "intentCode": 123456, + "metadata": { "orderId": "ORD-123" }, + "createdAt": "2025-01-01T12:00:00.000Z" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| id | string | Payment intent id. | +| accountId | string | Account id. | +| amount | string | Amount in smallest unit (e.g. 8 decimals). | +| ledgerCanisterId | string | Ledger canister id (token). | +| description | string \| null | Optional description. | +| expectedSenderPrincipal | string \| null | Optional expected sender address. | +| status | string | e.g. `requires_payment`, `processing`, `completed`, `failed`, `canceled`, `mismatched`. | +| intentCode | number | Intent code. | +| metadata | object | Key-value metadata from the intent. | +| createdAt | string | ISO 8601 timestamp. | + +## payment.created + +**Triggered when:** A payment record is created, or an existing payment is first linked to a transaction (e.g. when the chain sync attaches a transaction to the payment). + +**Emitted:** When a payment is saved or when a previously pending payment gets a `transactionId` for the first time. + +**data.object** (payment): + +```json +{ + "id": "pay_...", + "accountId": "acc_...", + "paymentIntentId": "pi_...", + "transactionId": "tx_...", + "transactionSplitId": "spl_...", + "canisterTxId": "123456", + "status": "pending", + "amount": "150000000", + "ledgerCanisterId": "ryjl3-tyaaa-aaaaa-aaaba-cai", + "ledgerTxId": "ltx_...", + "accountCanisterId": 42, + "basePaymentAccountId": "acc_base", + "invoiceId": null, + "metadata": { "orderId": "ORD-123" }, + "requestedAmount": "150000000", + "paidAmount": "140000000", + "createdAt": "2025-01-01T12:00:00.000Z" } ``` -### Payment object fields +| Field | Type | Description | +|-------|------|-------------| +| id | string | Payment id. | +| accountId | string | Account id. | +| paymentIntentId | string | Related payment intent id. | +| transactionId | string \| null | Related transaction id once linked. | +| transactionSplitId | string \| null | Transaction split id for this payment. | +| canisterTxId | string \| null | Chain/canister transaction id. | +| status | string | `pending`, `completed`, `failed`, `canceled`, `refunded`, `mismatched`. | +| amount | string | Amount in smallest unit. | +| ledgerCanisterId | string | Ledger (token) canister id. | +| ledgerTxId | string \| null | Ledger transaction id. | +| accountCanisterId | number \| null | Target account canister id. | +| basePaymentAccountId | string \| null | Base account id for split. | +| invoiceId | string \| null | Invoice id if created. | +| metadata | object | From the payment intent. | +| requestedAmount | string \| null | Intent amount (for mismatch comparison). | +| paidAmount | string \| null | Actual paid amount from transaction. | +| createdAt | string | ISO 8601. | + +If the payment is created with status `failed`, a `payment.failed` event is also emitted. + +## payment.updated + +**Triggered when:** A payment is updated and the new status is not one of completed, failed, cancelled, or refunded (e.g. status or other attributes change while still processing). + +**data.object**: Same shape as **payment.created** (see above). **data.previous_attributes** may contain the previous values of changed fields (e.g. `{ "status": "pending" }`). + +## payment.completed + +**Triggered when:** A payment’s status becomes `completed` (funds received and payment recorded). + +**data.object**: Same payment shape as **payment.created**. Typically includes `transactionId`, `ledgerTxId`, `amount`, and optional `requestedAmount` / `paidAmount` for mismatch handling. Use this event to fulfill orders or unlock content. + +## payment.failed + +**Triggered when:** A payment’s status becomes `failed`, or a payment is created with status `failed`. + +**data.object**: Same payment shape as **payment.created**. Use this event to notify the user or retry logic. + +## payment.cancelled + +**Triggered when:** A payment’s status becomes `canceled`. + +**data.object**: Same payment shape as **payment.created** with `status: "canceled"`. + +## payment.refunded + +**Triggered when:** A payment’s status becomes `refunded`. + +**data.object**: Same payment shape as **payment.created** with `status: "refunded"`. Use this event to update your records or inventory. + +--- + +### Payment object fields (summary) -- **transactionSplitId**: Identifier of the `TransactionSplit` that this payment represents. -- **basePaymentAccountId**: The base account for the intent/transaction (always present with ≥ 0.01% in split rules). -- **ledgerTxId**: Linked `LedgerTransaction` id once sweep/linking completes. +- **transactionSplitId**: Id of the transaction split that this payment represents. +- **basePaymentAccountId**: Base account for the intent/transaction (e.g. split rules). +- **ledgerTxId**: Linked ledger transaction id after sweep/linking. - **accountCanisterId**: Canister id of the target account for this payment. +- When **status** is `mismatched`, **requestedAmount** and **paidAmount** indicate the requested vs actual paid amount. ## Verify signatures Every request includes: -- `X-ICPay-Signature`: `t=,v1=` HMAC-SHA256 over `"." + payload` using your account secret key -- `X-ICPay-Timestamp`: Unix seconds +- **X-ICPay-Signature**: `t=,v1=` — HMAC-SHA256 over `"." + payload` using your account **secret key**. +- **X-ICPay-Timestamp**: Unix seconds (use for replay tolerance). ```ts {{ title: 'Node/Express verification' }} import crypto from 'crypto' @@ -143,8 +243,10 @@ app.post('/webhooks/icpay', express.raw({ type: 'application/json' }), (req, res if (!ok) return res.status(401).send('Invalid signature') const event = JSON.parse(req.body.toString('utf8')) switch (event.type) { + case 'payment_intent.created': /* e.g. log or prepare order */ break case 'payment.completed': /* fulfill order */ break case 'payment.failed': /* notify user */ break + case 'payment.refunded': /* update records */ break default: /* log */ } res.sendStatus(200) @@ -167,11 +269,11 @@ export async function POST(req: Request) { ## Best practices -- Verify signatures and enforce timestamp tolerance. -- Respond within 5s; perform heavy work async. -- Implement idempotency using event id `evt_*`. -- Log deliveries and store last processed `evt_*`. -- Use retries prudently; keep endpoints highly available. +- **Verify signatures** and enforce a timestamp tolerance (e.g. 300 seconds) to prevent replay. +- **Respond within ~5s**; do heavy work asynchronously. +- **Implement idempotency** using the event `id` (`evt_*`); store last processed ids to avoid duplicate handling. +- **Log deliveries** and failures for debugging. +- Use retries wisely; keep your endpoint highly available.