Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ yarn-error.log*
*.pem
dump.rdb

local/
local/
tsconfig.tsbuildinfo

# Claude Code
.claude/
8 changes: 8 additions & 0 deletions examples/nextjs-solana-subscriptions/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Required: Dynamic environment ID from https://app.dynamic.xyz
NEXT_PUBLIC_DYNAMIC_ENV_ID=your-environment-id-here

# Required: Merchant wallet address whose subscription plans to display
NEXT_PUBLIC_MERCHANT_ADDRESS=your-merchant-wallet-address-here

# Optional: Token mint for subscriptions (defaults to mainnet USDC)
NEXT_PUBLIC_TOKEN_MINT=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
57 changes: 57 additions & 0 deletions examples/nextjs-solana-subscriptions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Solana Subscriptions with Dynamic

A Next.js demo that integrates [Dynamic's](https://dynamic.xyz) embedded Solana wallets with the [Solana Subscriptions program](https://solana.com/docs/payments/subscriptions/overview) — letting users subscribe to on-chain recurring payment plans and manage their subscriptions.

## What it does

- Sign in with email OTP, Google OAuth, or an injected Solana wallet via Dynamic
- Browse subscription plans created by a configured merchant
- **Fund your wallet from any chain/token** via Dynamic Checkout — pay with SOL, ETH, or USDC on Base; receive USDC on Solana automatically
- Subscribe to a plan (initializes a subscription authority and creates a subscription delegation on-chain)
- View and cancel active subscriptions

## How it works

The Solana Subscriptions program lets merchants create on-chain plans with a fixed amount and period. Users subscribe by delegating token transfer rights to a subscription authority PDA. Merchants then call `transferSubscription` to collect recurring payments without needing the user to sign each time.

### Transaction flow

1. **initSubscriptionAuthority** (once per user + token mint): Creates a PDA that controls which subscriptions can pull from the user's token account.
2. **subscribe**: Creates a `SubscriptionDelegation` PDA recording the plan terms. The merchant can pull up to `amount` per `periodHours` until the subscription is cancelled.
3. **cancelSubscription**: Sets an `expiresAtTs` on the delegation — future pulls are blocked once that timestamp is reached.

Dynamic's embedded wallet handles all signing. The app uses `@solana/kit` v6 to build the subscription instructions and bridges them to `@solana/web3.js` v1 `VersionedTransaction` for Dynamic to sign.

## Setup

```bash
cp .env.example .env.local
# Fill in your values
pnpm install
pnpm dev
```

### Environment variables

| Variable | Required | Description |
|----------|----------|-------------|
| `NEXT_PUBLIC_DYNAMIC_ENV_ID` | Yes | Dynamic environment ID from [app.dynamic.xyz](https://app.dynamic.xyz) |
| `NEXT_PUBLIC_SOLANA_RPC_URL` | Yes | Solana mainnet RPC URL — must support `getProgramAccounts` (e.g. Helius, QuickNode) |
| `NEXT_PUBLIC_MERCHANT_ADDRESS` | Yes | Merchant wallet address whose plans to display |
| `DYNAMIC_API_TOKEN` | Yes (for Checkout) | Server-side API token from [developer settings](https://app.dynamic.xyz/dashboard/developer/api) |
| `NEXT_PUBLIC_TOKEN_MINT` | No | Token mint address (defaults to mainnet USDC) |
| `NEXT_PUBLIC_CHECKOUT_SETTLEMENT_TOKEN` | No | USDC mint for checkout settlement (defaults to mainnet USDC) |

### Dynamic dashboard

1. Enable **Solana** under Chains & Networks
2. Enable **Embedded wallets** under Wallets
3. Enable **Email OTP** and/or **Google** under Sign-in Methods
4. Add `http://localhost:3000` under Security → Allowed Origins

## Resources

- [Solana Subscriptions documentation](https://solana.com/docs/payments/subscriptions/overview)
- [Dynamic JS SDK](https://docs.dynamic.xyz/javascript/reference/quickstart)
- [`@solana/subscriptions` npm package](https://www.npmjs.com/package/@solana/subscriptions)
- [Demo webapp](https://solana-subscriptions-program.vercel.app/)
6 changes: 6 additions & 0 deletions examples/nextjs-solana-subscriptions/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
54 changes: 54 additions & 0 deletions examples/nextjs-solana-subscriptions/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { NextConfig } from "next";
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { NormalModuleReplacementPlugin } = require("webpack");

const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**",
},
],
},
webpack: (config, { isServer }) => {
config.experiments = {
...config.experiments,
asyncWebAssembly: true,
layers: true,
};

config.module.rules.push({
test: /\.wasm$/,
type: "webassembly/async",
});

if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
path: false,
crypto: false,
stream: false,
os: false,
http: false,
https: false,
zlib: false,
};
}

config.plugins.push(
new NormalModuleReplacementPlugin(
/@evervault\/wasm-attestation-bindings/,
(resource: { request: string }) => {
resource.request =
"data:text/javascript,const PCRs={};const validateAttestationDocPcrs=()=>Promise.resolve();const getUserData=()=>Promise.resolve();const getNonce=()=>Promise.resolve();const init=()=>Promise.resolve();export default init;export{PCRs,validateAttestationDocPcrs,getUserData,getNonce};";
}
)
);

return config;
},
};

export default nextConfig;
52 changes: 52 additions & 0 deletions examples/nextjs-solana-subscriptions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "nextjs-solana-subscriptions",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"type-check": "tsc --noEmit",
"clean": "rm -rf .next && rm -rf node_modules/.cache"
},
"dependencies": {
"@dynamic-labs-sdk/client": "1.4.0",
"@dynamic-labs-sdk/react-hooks": "1.4.0",
"@dynamic-labs-sdk/solana": "1.4.0",
"@solana-program/token": "^0.13.0",
"@solana/kit": "6.9.0",
"@solana/subscriptions": "0.2.0",
"@solana/web3.js": "1.98.4",
"@tanstack/react-query": "5.100.9",
"clsx": "2.1.1",
"lucide-react": "0.542.0",
"next": "15.5.9",
"react": "19.2.6",
"react-dom": "19.2.6",
"tailwind-merge": "3.5.0"
},
"devDependencies": {
"@eslint/eslintrc": "3.3.5",
"@tailwindcss/postcss": "4.2.4",
"@types/node": "24.3.0",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"autoprefixer": "10.5.0",
"eslint": "9.34.0",
"eslint-config-next": "15.5.2",
"postcss": "8.5.14",
"tailwindcss": "4.2.4",
"typescript": "5.9.2"
},
"browser": {
"crypto": false
},
"pnpm": {
"overrides": {
"@solana/web3.js": "1.98.4",
"@dynamic-labs-sdk/client": "1.4.0"
}
}
}
Loading