stripe not working yet, broken on sync
This commit is contained in:
parent
7b4994fb3e
commit
6cb69c9bc4
|
|
@ -0,0 +1,28 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
- `npm run dev` - Run development servers (client + worker)
|
||||||
|
- `npm run build` - Build for production
|
||||||
|
- `npm run deploy` - Build and deploy to Vercel/Cloudflare
|
||||||
|
- `npm run types` - TypeScript type checking
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
- **TypeScript**: Use strict mode, explicit return types, and proper interfaces for props
|
||||||
|
- **Formatting**: No semicolons, trailing commas for all elements
|
||||||
|
- **Naming**: PascalCase for components/types, camelCase for utilities/functions
|
||||||
|
- **Imports**: Group related imports, React imports first, use absolute paths with aliases
|
||||||
|
- **Error Handling**: Catch and log errors with console.error, return success/failure values
|
||||||
|
- **Components**: Separate UI from business logic, use functional components with hooks
|
||||||
|
- **State**: Use React Context for global state, follow immutable update patterns
|
||||||
|
- **Documentation**: Include JSDoc comments for functions and modules
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
- `/src/components/` - UI components organized by feature
|
||||||
|
- `/src/context/` - React Context providers
|
||||||
|
- `/src/lib/` - Business logic and utilities
|
||||||
|
- `/src/routes/` - Page definitions
|
||||||
|
- `/src/css/` - Styles organized by feature
|
||||||
|
- `/src/ui/` - Reusable UI components
|
||||||
|
- `/worker/` - Cloudflare Workers backend code
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
# Stripe Integration Setup for Canvas Website
|
||||||
|
|
||||||
|
This document outlines the setup process for integrating Stripe payments into the canvas website, specifically for subscription plans.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Stripe integration allows users to create subscription payment forms directly on the canvas. Users can select from predefined subscription plans and complete their subscription using Stripe's secure payment processing.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Subscription Plans**: Three predefined plans (Basic, Pro, Enterprise)
|
||||||
|
- **Interactive UI**: Plan selection with feature comparison
|
||||||
|
- **Secure Payments**: Stripe Elements integration
|
||||||
|
- **Webhook Support**: Real-time subscription event handling
|
||||||
|
- **Customer Management**: Automatic customer creation and management
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Install Stripe CLI
|
||||||
|
|
||||||
|
First, install the Stripe CLI on your system:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For Ubuntu/Debian
|
||||||
|
curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg --dearmor | sudo tee /usr/share/keyrings/stripe.gpg
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" | sudo tee -a /etc/apt/sources.list.d/stripe.list
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install stripe
|
||||||
|
|
||||||
|
# For macOS
|
||||||
|
brew install stripe/stripe-cli/stripe
|
||||||
|
|
||||||
|
# For Windows
|
||||||
|
# Download from https://github.com/stripe/stripe-cli/releases
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Login to Stripe
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stripe login
|
||||||
|
```
|
||||||
|
|
||||||
|
This will open your browser to authenticate with your Stripe account.
|
||||||
|
|
||||||
|
### 3. Get Your API Keys
|
||||||
|
|
||||||
|
1. Go to your [Stripe Dashboard](https://dashboard.stripe.com/)
|
||||||
|
2. Navigate to **Developers** > **API keys**
|
||||||
|
3. Copy your **Publishable key** and **Secret key**
|
||||||
|
|
||||||
|
### 4. Set Environment Variables
|
||||||
|
|
||||||
|
Create or update your `.dev.vars` file with the following variables:
|
||||||
|
|
||||||
|
```env
|
||||||
|
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
|
||||||
|
STRIPE_SECRET_KEY=sk_test_your_secret_key_here
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Set Up Webhooks
|
||||||
|
|
||||||
|
Run the following command to start listening for webhook events:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stripe listen --forward-to localhost:8787/api/stripe/webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
This will output a webhook secret. Copy this secret and add it to your `.dev.vars` file as `STRIPE_WEBHOOK_SECRET`.
|
||||||
|
|
||||||
|
### 6. Create Subscription Products and Prices
|
||||||
|
|
||||||
|
You'll need to create the subscription products and prices in your Stripe dashboard that correspond to the plans defined in the code:
|
||||||
|
|
||||||
|
#### Basic Plan
|
||||||
|
- Product Name: "Basic Plan"
|
||||||
|
- Price: $9.99/month
|
||||||
|
- Price ID: Use this ID in the code (currently set to 'basic')
|
||||||
|
|
||||||
|
#### Pro Plan
|
||||||
|
- Product Name: "Pro Plan"
|
||||||
|
- Price: $19.99/month
|
||||||
|
- Price ID: Use this ID in the code (currently set to 'pro')
|
||||||
|
|
||||||
|
#### Enterprise Plan
|
||||||
|
- Product Name: "Enterprise Plan"
|
||||||
|
- Price: $49.99/month
|
||||||
|
- Price ID: Use this ID in the code (currently set to 'enterprise')
|
||||||
|
|
||||||
|
### 7. Update Price IDs
|
||||||
|
|
||||||
|
After creating the products and prices in Stripe, update the `SUBSCRIPTION_PLANS` array in `src/shapes/stripe/StripePaymentShapeUtil.tsx` with the actual Stripe price IDs:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const SUBSCRIPTION_PLANS = [
|
||||||
|
{
|
||||||
|
id: 'price_actual_stripe_price_id_here', // Replace with actual Stripe price ID
|
||||||
|
name: 'Basic Plan',
|
||||||
|
price: 999,
|
||||||
|
interval: 'month',
|
||||||
|
description: 'Perfect for individuals',
|
||||||
|
features: ['Basic features', 'Email support', '1GB storage']
|
||||||
|
},
|
||||||
|
// ... update other plans similarly
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
The integration provides the following API endpoints:
|
||||||
|
|
||||||
|
- `POST /api/stripe/create-subscription` - Creates a new subscription
|
||||||
|
- `POST /api/stripe/create-payment-intent` - Creates a payment intent (for one-time payments)
|
||||||
|
- `POST /api/stripe/webhook` - Handles Stripe webhook events
|
||||||
|
|
||||||
|
## Webhook Events
|
||||||
|
|
||||||
|
The webhook handler processes the following events:
|
||||||
|
|
||||||
|
- `customer.subscription.created` - New subscription created
|
||||||
|
- `customer.subscription.updated` - Subscription updated
|
||||||
|
- `customer.subscription.deleted` - Subscription deleted
|
||||||
|
- `invoice.payment_succeeded` - Invoice payment successful
|
||||||
|
- `invoice.payment_failed` - Invoice payment failed
|
||||||
|
- `payment_intent.succeeded` - Payment intent successful
|
||||||
|
- `payment_intent.payment_failed` - Payment intent failed
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Adding a Subscription Form to Canvas
|
||||||
|
|
||||||
|
1. Use the Stripe tool from the toolbar (shortcut: `Alt+Shift+P`)
|
||||||
|
2. Select a subscription plan from the available options
|
||||||
|
3. Enter customer email (optional)
|
||||||
|
4. Choose theme preference
|
||||||
|
5. Click "Subscribe" to initialize the payment form
|
||||||
|
6. Complete payment details using Stripe Elements
|
||||||
|
|
||||||
|
### Keyboard Shortcuts
|
||||||
|
|
||||||
|
- `Alt+Shift+P` - Add Stripe subscription form
|
||||||
|
- `Alt+Shift+S` - Quick action to add subscription form
|
||||||
|
|
||||||
|
### Context Menu
|
||||||
|
|
||||||
|
Right-click on the canvas to access the "Add Stripe Subscription" option.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test Cards
|
||||||
|
|
||||||
|
Use these test card numbers for testing:
|
||||||
|
|
||||||
|
- **Success**: `4242 4242 4242 4242`
|
||||||
|
- **Decline**: `4000 0000 0000 0002`
|
||||||
|
- **Requires Authentication**: `4000 0025 0000 3155`
|
||||||
|
|
||||||
|
### Test Mode
|
||||||
|
|
||||||
|
The integration runs in test mode by default. All transactions are test transactions and won't result in actual charges.
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Never expose your Stripe secret key in client-side code
|
||||||
|
- Always verify webhook signatures
|
||||||
|
- Use HTTPS in production
|
||||||
|
- Implement proper error handling
|
||||||
|
- Validate all input data
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Webhook not receiving events**: Ensure the webhook endpoint is accessible and the secret is correct
|
||||||
|
2. **Payment form not loading**: Check that the publishable key is correct
|
||||||
|
3. **Subscription creation fails**: Verify the price IDs exist in your Stripe account
|
||||||
|
4. **CORS errors**: Ensure the worker is properly configured for CORS
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable debug logging by setting the appropriate environment variables or checking the browser console and worker logs.
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
When deploying to production:
|
||||||
|
|
||||||
|
1. Switch to live API keys
|
||||||
|
2. Update webhook endpoints to production URLs
|
||||||
|
3. Configure proper CORS settings
|
||||||
|
4. Set up monitoring and alerting
|
||||||
|
5. Test the complete subscription flow
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues related to:
|
||||||
|
- **Stripe API**: Check [Stripe Documentation](https://stripe.com/docs)
|
||||||
|
- **Integration**: Review this setup guide and check the code comments
|
||||||
|
- **Canvas Website**: Refer to the main project documentation
|
||||||
19
index.html
19
index.html
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>Jeff Emmett</title>
|
<title>Jeff Emmett</title>
|
||||||
|
|
@ -33,6 +33,23 @@
|
||||||
<!-- Analytics -->
|
<!-- Analytics -->
|
||||||
<script data-goatcounter="https://jeff.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
|
<script data-goatcounter="https://jeff.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Check if we're on a subscription success page
|
||||||
|
if (window.location.pathname === '/subscription-success') {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div style="display: flex; justify-content: center; align-items: center; height: 100vh; font-family: system-ui, sans-serif;">
|
||||||
|
<div style="text-align: center; padding: 40px; border-radius: 8px; background: #f8f9fa; border: 1px solid #e9ecef;">
|
||||||
|
<h1 style="color: #28a745; margin-bottom: 16px;">🎉 Subscription Successful!</h1>
|
||||||
|
<p style="margin-bottom: 24px; color: #6c757d;">Your subscription has been activated successfully.</p>
|
||||||
|
<button onclick="window.location.href='/'" style="padding: 12px 24px; background: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px;">
|
||||||
|
Return to Canvas
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
"@anthropic-ai/sdk": "^0.33.1",
|
"@anthropic-ai/sdk": "^0.33.1",
|
||||||
"@daily-co/daily-js": "^0.60.0",
|
"@daily-co/daily-js": "^0.60.0",
|
||||||
"@daily-co/daily-react": "^0.20.0",
|
"@daily-co/daily-react": "^0.20.0",
|
||||||
|
"@stripe/react-stripe-js": "^3.7.0",
|
||||||
|
"@stripe/stripe-js": "^7.3.1",
|
||||||
"@tldraw/assets": "^3.6.0",
|
"@tldraw/assets": "^3.6.0",
|
||||||
"@tldraw/sync": "^3.6.0",
|
"@tldraw/sync": "^3.6.0",
|
||||||
"@tldraw/sync-core": "^3.6.0",
|
"@tldraw/sync-core": "^3.6.0",
|
||||||
|
|
@ -25,6 +27,7 @@
|
||||||
"cherry-markdown": "^0.8.57",
|
"cherry-markdown": "^0.8.57",
|
||||||
"cloudflare-workers-unfurl": "^0.0.7",
|
"cloudflare-workers-unfurl": "^0.0.7",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
"holosphere": "^1.1.17",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"itty-router": "^5.0.17",
|
"itty-router": "^5.0.17",
|
||||||
"jotai": "^2.6.0",
|
"jotai": "^2.6.0",
|
||||||
|
|
@ -38,6 +41,7 @@
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.0.2",
|
"react-router-dom": "^7.0.2",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
|
"stripe": "^18.2.1",
|
||||||
"tldraw": "^3.6.0",
|
"tldraw": "^3.6.0",
|
||||||
"vercel": "^39.1.1"
|
"vercel": "^39.1.1"
|
||||||
},
|
},
|
||||||
|
|
@ -1847,6 +1851,48 @@
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@peculiar/asn1-schema": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-umbembjIWOrPSOzEGG5vxFLkeM8kzIhLkgigtsOrfLKnuzxWxejAcUX+q/SoZCdemlODOcr5WiYa7+dIEzBXZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"asn1js": "^3.0.6",
|
||||||
|
"pvtsutils": "^1.3.6",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/json-schema": {
|
||||||
|
"version": "1.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz",
|
||||||
|
"integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@peculiar/webcrypto": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
|
"@peculiar/json-schema": "^1.1.12",
|
||||||
|
"pvtsutils": "^1.3.5",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"webcrypto-core": "^1.8.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/number": {
|
"node_modules/@radix-ui/number": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
|
||||||
|
|
@ -3012,6 +3058,29 @@
|
||||||
"integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==",
|
"integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@stripe/react-stripe-js": {
|
||||||
|
"version": "3.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.7.0.tgz",
|
||||||
|
"integrity": "sha512-PYls/2S9l0FF+2n0wHaEJsEU8x7CmBagiH7zYOsxbBlLIHEsqUIQ4MlIAbV9Zg6xwT8jlYdlRIyBTHmO3yM7kQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.7.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@stripe/stripe-js": ">=1.44.1 <8.0.0",
|
||||||
|
"react": ">=16.8.0 <20.0.0",
|
||||||
|
"react-dom": ">=16.8.0 <20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@stripe/stripe-js": {
|
||||||
|
"version": "7.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.3.1.tgz",
|
||||||
|
"integrity": "sha512-pTzb864TQWDRQBPLgSPFRoyjSDUqpCkbEgTzpsjiTjGz1Z5SxZNXJek28w1s6Dyry4CyW4/Izj5jHE/J9hCJYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tldraw/assets": {
|
"node_modules/@tldraw/assets": {
|
||||||
"version": "3.6.1",
|
"version": "3.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tldraw/assets/-/assets-3.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tldraw/assets/-/assets-3.6.1.tgz",
|
||||||
|
|
@ -4286,6 +4355,21 @@
|
||||||
"printable-characters": "^1.0.42"
|
"printable-characters": "^1.0.42"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asn1js": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"pvtsutils": "^1.3.6",
|
||||||
|
"pvutils": "^1.1.3",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/async-listen": {
|
"node_modules/async-listen": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/async-listen/-/async-listen-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/async-listen/-/async-listen-1.2.0.tgz",
|
||||||
|
|
@ -4470,6 +4554,35 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001690",
|
"version": "1.0.30001690",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
|
||||||
|
|
@ -5644,6 +5757,20 @@
|
||||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/edge-runtime": {
|
"node_modules/edge-runtime": {
|
||||||
"version": "2.5.9",
|
"version": "2.5.9",
|
||||||
"resolved": "https://registry.npmjs.org/edge-runtime/-/edge-runtime-2.5.9.tgz",
|
"resolved": "https://registry.npmjs.org/edge-runtime/-/edge-runtime-2.5.9.tgz",
|
||||||
|
|
@ -5723,12 +5850,42 @@
|
||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-module-lexer": {
|
"node_modules/es-module-lexer": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
|
||||||
"integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==",
|
"integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.14.47",
|
"version": "0.14.47",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.47.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.47.tgz",
|
||||||
|
|
@ -6317,6 +6474,22 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-uri": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.18.0",
|
"version": "1.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
|
||||||
|
|
@ -6458,6 +6631,15 @@
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gauge": {
|
"node_modules/gauge": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
|
||||||
|
|
@ -6514,6 +6696,30 @@
|
||||||
"node": "6.* || 8.* || >= 10.*"
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-nonce": {
|
"node_modules/get-nonce": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||||
|
|
@ -6523,6 +6729,19 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-source": {
|
"node_modules/get-source": {
|
||||||
"version": "2.0.12",
|
"version": "2.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz",
|
||||||
|
|
@ -6605,6 +6824,18 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
|
@ -6626,6 +6857,53 @@
|
||||||
"node": ">=6.0"
|
"node": ">=6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gun": {
|
||||||
|
"version": "0.2020.1241",
|
||||||
|
"resolved": "https://registry.npmjs.org/gun/-/gun-0.2020.1241.tgz",
|
||||||
|
"integrity": "sha512-rmGqLuJj4fAuZ/0lddCvXHbENPkEnBOBYpq+kXHrwQ5RdNtQ5p0Io99lD1qUXMFmtwNacQ/iqo3VTmjmMyAYZg==",
|
||||||
|
"license": "(Zlib OR MIT OR Apache-2.0)",
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^7.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.4"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@peculiar/webcrypto": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gun/node_modules/ws": {
|
||||||
|
"version": "7.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||||
|
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": "^5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/h3-js": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/h3-js/-/h3-js-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-HYiUrq5qTRFqMuQu3jEHqxXLk1zsSJiby9Lja/k42wHjabZG7tN9rOuzT/PEFf+Wa7rsnHLMHRWIu0mgcJ0ewQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4",
|
||||||
|
"npm": ">=3",
|
||||||
|
"yarn": ">=1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hamt_plus": {
|
"node_modules/hamt_plus": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
|
||||||
|
|
@ -6642,12 +6920,36 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-unicode": {
|
"node_modules/has-unicode": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||||
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
|
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hast-util-from-html": {
|
"node_modules/hast-util-from-html": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
|
||||||
|
|
@ -6936,6 +7238,49 @@
|
||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/holosphere": {
|
||||||
|
"version": "1.1.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/holosphere/-/holosphere-1.1.17.tgz",
|
||||||
|
"integrity": "sha512-tQsP9lFoOnU1KDqU2s+TZTj3P5GCzN8DXtl2VXSIg1DY+8SoflqdI7nFAfW8JrgqIBvRTKKq38Zw22gdrhGZnA==",
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.12.0",
|
||||||
|
"gun": "^0.2020.1240",
|
||||||
|
"h3-js": "^4.1.0",
|
||||||
|
"openai": "^4.85.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"gun": "^0.2020.1240",
|
||||||
|
"h3-js": "^4.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"openai": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/holosphere/node_modules/ajv": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/holosphere/node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/hotkeys-js": {
|
"node_modules/hotkeys-js": {
|
||||||
"version": "3.13.9",
|
"version": "3.13.9",
|
||||||
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.9.tgz",
|
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.9.tgz",
|
||||||
|
|
@ -7635,6 +7980,15 @@
|
||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mdast-util-find-and-replace": {
|
"node_modules/mdast-util-find-and-replace": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
|
||||||
|
|
@ -8906,6 +9260,18 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ohash": {
|
"node_modules/ohash": {
|
||||||
"version": "2.0.11",
|
"version": "2.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
|
||||||
|
|
@ -8938,9 +9304,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/openai": {
|
"node_modules/openai": {
|
||||||
"version": "4.79.3",
|
"version": "4.104.0",
|
||||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.79.3.tgz",
|
"resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
|
||||||
"integrity": "sha512-0yAnr6oxXAyVrYwLC1jA0KboyU7DjEmrfTXQX+jSpE+P4i72AI/Lxx5pvR3r9i5X7G33835lL+ZrnQ+MDvyuUg==",
|
"integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
|
|
@ -9185,6 +9551,17 @@
|
||||||
"integrity": "sha512-V6TbZDJ/ZswevgkDNpGt/YqNCiZP9ASfgU+p83uJE6NrGtvSGoOcHLiDCqkMs2+yg7F5qHdLV8d0aS8O26G/KA==",
|
"integrity": "sha512-V6TbZDJ/ZswevgkDNpGt/YqNCiZP9ASfgU+p83uJE6NrGtvSGoOcHLiDCqkMs2+yg7F5qHdLV8d0aS8O26G/KA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/prop-types": {
|
||||||
|
"version": "15.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/property-information": {
|
"node_modules/property-information": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz",
|
||||||
|
|
@ -9226,6 +9603,41 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pvtsutils": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pvutils": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||||
|
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/querystringify": {
|
"node_modules/querystringify": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||||
|
|
@ -9345,6 +9757,12 @@
|
||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react-markdown": {
|
"node_modules/react-markdown": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
||||||
|
|
@ -10241,6 +10659,78 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-list": "^1.0.0",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz",
|
||||||
|
|
@ -10460,6 +10950,26 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stripe": {
|
||||||
|
"version": "18.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/stripe/-/stripe-18.2.1.tgz",
|
||||||
|
"integrity": "sha512-GwB1B7WSwEBzW4dilgyJruUYhbGMscrwuyHsPUmSRKrGHZ5poSh2oU9XKdii5BFVJzXHn35geRvGJ6R8bYcp8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"qs": "^6.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=12.x.x"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/style-to-js": {
|
"node_modules/style-to-js": {
|
||||||
"version": "1.1.16",
|
"version": "1.1.16",
|
||||||
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz",
|
||||||
|
|
@ -11328,6 +11838,20 @@
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/webcrypto-core": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@peculiar/asn1-schema": "^2.3.13",
|
||||||
|
"@peculiar/json-schema": "^1.1.12",
|
||||||
|
"asn1js": "^3.0.5",
|
||||||
|
"pvtsutils": "^1.3.5",
|
||||||
|
"tslib": "^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@
|
||||||
"@anthropic-ai/sdk": "^0.33.1",
|
"@anthropic-ai/sdk": "^0.33.1",
|
||||||
"@daily-co/daily-js": "^0.60.0",
|
"@daily-co/daily-js": "^0.60.0",
|
||||||
"@daily-co/daily-react": "^0.20.0",
|
"@daily-co/daily-react": "^0.20.0",
|
||||||
|
"@stripe/react-stripe-js": "^3.7.0",
|
||||||
|
"@stripe/stripe-js": "^7.3.1",
|
||||||
"@tldraw/assets": "^3.6.0",
|
"@tldraw/assets": "^3.6.0",
|
||||||
"@tldraw/sync": "^3.6.0",
|
"@tldraw/sync": "^3.6.0",
|
||||||
"@tldraw/sync-core": "^3.6.0",
|
"@tldraw/sync-core": "^3.6.0",
|
||||||
|
|
@ -32,6 +34,7 @@
|
||||||
"cherry-markdown": "^0.8.57",
|
"cherry-markdown": "^0.8.57",
|
||||||
"cloudflare-workers-unfurl": "^0.0.7",
|
"cloudflare-workers-unfurl": "^0.0.7",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
"holosphere": "^1.1.17",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"itty-router": "^5.0.17",
|
"itty-router": "^5.0.17",
|
||||||
"jotai": "^2.6.0",
|
"jotai": "^2.6.0",
|
||||||
|
|
@ -45,6 +48,7 @@
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.0.2",
|
"react-router-dom": "^7.0.2",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
|
"stripe": "^18.2.1",
|
||||||
"tldraw": "^3.6.0",
|
"tldraw": "^3.6.0",
|
||||||
"vercel": "^39.1.1"
|
"vercel": "^39.1.1"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ import {
|
||||||
initLockIndicators,
|
initLockIndicators,
|
||||||
watchForLockedShapes,
|
watchForLockedShapes,
|
||||||
} from "@/ui/cameraUtils"
|
} from "@/ui/cameraUtils"
|
||||||
|
import { StripePaymentShapeUtil } from "@/shapes/stripe/StripePaymentShapeUtil"
|
||||||
|
import { StripePaymentTool } from "@/tools/StripePaymentTool"
|
||||||
|
|
||||||
// Default to production URL if env var isn't available
|
// Default to production URL if env var isn't available
|
||||||
export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev"
|
export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev"
|
||||||
|
|
@ -49,6 +51,7 @@ const customShapeUtils = [
|
||||||
MycrozineTemplateShape,
|
MycrozineTemplateShape,
|
||||||
MarkdownShape,
|
MarkdownShape,
|
||||||
PromptShape,
|
PromptShape,
|
||||||
|
StripePaymentShapeUtil,
|
||||||
]
|
]
|
||||||
const customTools = [
|
const customTools = [
|
||||||
ChatBoxTool,
|
ChatBoxTool,
|
||||||
|
|
@ -58,6 +61,7 @@ const customTools = [
|
||||||
MycrozineTemplateTool,
|
MycrozineTemplateTool,
|
||||||
MarkdownTool,
|
MarkdownTool,
|
||||||
PromptShapeTool,
|
PromptShapeTool,
|
||||||
|
StripePaymentTool,
|
||||||
]
|
]
|
||||||
|
|
||||||
export function Board() {
|
export function Board() {
|
||||||
|
|
@ -97,6 +101,13 @@ export function Board() {
|
||||||
watchForLockedShapes(editor)
|
watchForLockedShapes(editor)
|
||||||
}, [editor])
|
}, [editor])
|
||||||
|
|
||||||
|
// Cleanup global editor reference on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
delete (window as any).__TLDRAW_EDITOR__
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "fixed", inset: 0 }}>
|
<div style={{ position: "fixed", inset: 0 }}>
|
||||||
<Tldraw
|
<Tldraw
|
||||||
|
|
@ -136,6 +147,8 @@ export function Board() {
|
||||||
}}
|
}}
|
||||||
onMount={(editor) => {
|
onMount={(editor) => {
|
||||||
setEditor(editor)
|
setEditor(editor)
|
||||||
|
// Expose editor globally for Stripe component access
|
||||||
|
;(window as any).__TLDRAW_EDITOR__ = editor
|
||||||
editor.registerExternalAssetHandler("url", unfurlBookmarkUrl)
|
editor.registerExternalAssetHandler("url", unfurlBookmarkUrl)
|
||||||
editor.setCurrentTool("hand")
|
editor.setCurrentTool("hand")
|
||||||
setInitialCameraFromUrl(editor)
|
setInitialCameraFromUrl(editor)
|
||||||
|
|
|
||||||
|
|
@ -112,15 +112,15 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
|
||||||
}}
|
}}
|
||||||
preview='live'
|
preview='live'
|
||||||
visibleDragbar={true}
|
visibleDragbar={true}
|
||||||
|
height={shape.props.h - 2}
|
||||||
style={{
|
style={{
|
||||||
height: 'auto',
|
width: '100%',
|
||||||
minHeight: '100%',
|
|
||||||
border: 'none',
|
border: 'none',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
}}
|
}}
|
||||||
previewOptions={{
|
previewOptions={{
|
||||||
style: {
|
style: {
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
@ -128,8 +128,6 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
|
||||||
style: {
|
style: {
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
lineHeight: '1.5',
|
lineHeight: '1.5',
|
||||||
height: 'auto',
|
|
||||||
minHeight: '100%',
|
|
||||||
resize: 'none',
|
resize: 'none',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
}
|
}
|
||||||
|
|
@ -137,6 +135,8 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
|
hideToolbar={false}
|
||||||
|
enableScroll={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { StripePaymentPopup } from './StripePaymentShapeUtil';
|
||||||
|
|
||||||
|
export function ModalManager() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
return <StripePaymentPopup onClose={() => setIsOpen(false)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: '20px',
|
||||||
|
right: '20px',
|
||||||
|
zIndex: 1000,
|
||||||
|
padding: '12px 24px',
|
||||||
|
backgroundColor: '#0066cc',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: '600',
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
💳 Open Payment
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { TLBaseShape } from 'tldraw';
|
||||||
|
|
||||||
|
export type StripePaymentShape = TLBaseShape<
|
||||||
|
'stripe-payment',
|
||||||
|
{
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
@ -0,0 +1,479 @@
|
||||||
|
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
BaseBoxShapeUtil,
|
||||||
|
HTMLContainer,
|
||||||
|
RecordProps,
|
||||||
|
T,
|
||||||
|
TLBaseShape,
|
||||||
|
} from 'tldraw';
|
||||||
|
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
||||||
|
import { loadStripe } from '@stripe/stripe-js';
|
||||||
|
|
||||||
|
// Declare global variable from vite config
|
||||||
|
declare const __STRIPE_PUBLISHABLE_KEY__: string;
|
||||||
|
|
||||||
|
// Define the shape type inline to avoid import conflicts
|
||||||
|
export type StripePaymentShape = TLBaseShape<
|
||||||
|
'stripe-payment',
|
||||||
|
{
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
// Define subscription plans
|
||||||
|
const SUBSCRIPTION_PLANS = [
|
||||||
|
{
|
||||||
|
id: 'price_1RdDMgKFe1dC1xn7p319KRDU', // Basic Support Stream
|
||||||
|
name: 'Basic Support Stream',
|
||||||
|
price: 500, // $5.00 CAD
|
||||||
|
interval: 'month',
|
||||||
|
description: 'I like what you\'re doing',
|
||||||
|
features: ['Yay support']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'price_1RdDMwKFe1dC1xn7kDSgE95J', // Mid-range support stream
|
||||||
|
name: 'Mid-range support stream',
|
||||||
|
price: 2500, // $25.00 CAD
|
||||||
|
interval: 'month',
|
||||||
|
description: 'Wait this stuff could actually be helpful',
|
||||||
|
features: ['Even Yayer']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'price_1RdDNAKFe1dC1xn7x2n0FUI5', // Comrades & Collaborators
|
||||||
|
name: 'Comrades & Collaborators',
|
||||||
|
price: 5000, // $50.00 CAD
|
||||||
|
interval: 'month',
|
||||||
|
description: 'We are the ones we\'ve been waiting for',
|
||||||
|
features: ['The yayest of them all']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Stripe Payment Form Component
|
||||||
|
function StripePaymentForm({ clientSecret }: { clientSecret: string }) {
|
||||||
|
const stripe = useStripe();
|
||||||
|
const elements = useElements();
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [processing, setProcessing] = useState(false);
|
||||||
|
const mountedRef = useRef(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
mountedRef.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!stripe || !elements || !mountedRef.current) return;
|
||||||
|
|
||||||
|
setProcessing(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { error } = await stripe.confirmPayment({
|
||||||
|
elements,
|
||||||
|
confirmParams: { return_url: window.location.href },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error && mountedRef.current) {
|
||||||
|
setError(error.message || 'Payment failed');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setError('An unexpected error occurred');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setProcessing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof __STRIPE_PUBLISHABLE_KEY__ === 'undefined' || !__STRIPE_PUBLISHABLE_KEY__) {
|
||||||
|
return <div style={{ color: '#dc3545', textAlign: 'center', padding: '20px' }}>
|
||||||
|
<div>Stripe configuration error. Please check your setup.</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripePromise = loadStripe(__STRIPE_PUBLISHABLE_KEY__);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Elements
|
||||||
|
stripe={stripePromise}
|
||||||
|
options={{
|
||||||
|
clientSecret,
|
||||||
|
appearance: {
|
||||||
|
theme: 'flat',
|
||||||
|
variables: {
|
||||||
|
colorPrimary: '#0066cc',
|
||||||
|
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
|
||||||
|
fontSizeBase: '16px',
|
||||||
|
spacingUnit: '8px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit} style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||||
|
<div style={{ flexShrink: 0 }}>
|
||||||
|
<h2 style={{ margin: '0 0 12px 0', fontSize: '24px', fontWeight: '700', color: '#1a1a1a', textAlign: 'center' }}>
|
||||||
|
Complete Your Subscription
|
||||||
|
</h2>
|
||||||
|
<p style={{ margin: 0, color: '#666', fontSize: '14px', textAlign: 'center', lineHeight: '1.4' }}>
|
||||||
|
Enter your payment details to start your subscription
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, marginBottom: '16px', minHeight: '180px' }}>
|
||||||
|
<PaymentElement />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div style={{
|
||||||
|
color: '#dc3545', marginBottom: '16px', fontSize: '14px',
|
||||||
|
padding: '12px', backgroundColor: '#f8d7da', border: '1px solid #f5c6cb',
|
||||||
|
borderRadius: '8px', textAlign: 'center'
|
||||||
|
}}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!stripe || processing}
|
||||||
|
style={{
|
||||||
|
width: '100%', padding: '14px',
|
||||||
|
backgroundColor: processing ? '#ccc' : '#0066cc',
|
||||||
|
color: 'white', border: 'none', borderRadius: '8px',
|
||||||
|
fontSize: '16px', fontWeight: '600',
|
||||||
|
cursor: processing ? 'not-allowed' : 'pointer',
|
||||||
|
transition: 'background-color 0.2s ease', minHeight: '44px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{processing ? 'Processing...' : 'Start Subscription'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</Elements>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stripe Payment Popup Component
|
||||||
|
export function StripePaymentPopup({ onClose }: { onClose: () => void }) {
|
||||||
|
const [selectedPlanId, setSelectedPlanId] = useState('price_1RdDMgKFe1dC1xn7p319KRDU');
|
||||||
|
const [customerEmail, setCustomerEmail] = useState('');
|
||||||
|
const [clientSecret, setClientSecret] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const mountedRef = useRef(true);
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
mountedRef.current = false;
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const initializeSubscription = useCallback(async () => {
|
||||||
|
// Only proceed if we have a valid email and aren't already loading
|
||||||
|
if (isLoading || clientSecret || !customerEmail.trim() || !mountedRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Create new abort controller for this request
|
||||||
|
abortControllerRef.current = new AbortController();
|
||||||
|
|
||||||
|
const selectedPlan = SUBSCRIPTION_PLANS.find(plan => plan.id === selectedPlanId) || SUBSCRIPTION_PLANS[0];
|
||||||
|
|
||||||
|
const response = await fetch('/api/stripe/create-subscription', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
priceId: selectedPlan.id,
|
||||||
|
customerEmail: customerEmail.trim(),
|
||||||
|
metadata: { planId: selectedPlan.id },
|
||||||
|
}),
|
||||||
|
signal: abortControllerRef.current.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mountedRef.current) return;
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json() as { client_secret: string; payment_intent_id?: string; customer_id?: string; price_id?: string };
|
||||||
|
if (!data.client_secret) {
|
||||||
|
throw new Error('No client secret received from server');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setClientSecret(data.client_secret);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (mountedRef.current) {
|
||||||
|
console.error('Stripe: Subscription initialization error:', err);
|
||||||
|
setError('Failed to initialize subscription. Please try again.');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [selectedPlanId, customerEmail, isLoading, clientSecret]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0, left: 0, right: 0, bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 10000,
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: '600px',
|
||||||
|
height: '700px',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '20px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '16px', right: '16px',
|
||||||
|
width: '32px', height: '32px',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
borderRadius: '50%',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '18px',
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{!clientSecret ? (
|
||||||
|
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: '16px', overflow: 'hidden' }}>
|
||||||
|
<h2 style={{ margin: '0 0 16px 0', fontSize: '24px', fontWeight: '700', color: '#1a1a1a', textAlign: 'center' }}>
|
||||||
|
Choose Your Plan
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px', flex: 1, overflow: 'auto' }}>
|
||||||
|
{SUBSCRIPTION_PLANS.map((plan) => (
|
||||||
|
<div
|
||||||
|
key={plan.id}
|
||||||
|
style={{
|
||||||
|
padding: '16px',
|
||||||
|
border: `2px solid ${selectedPlanId === plan.id ? '#0066cc' : '#e0e0e0'}`,
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: selectedPlanId === plan.id ? '#f0f8ff' : 'transparent',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
minHeight: '100px',
|
||||||
|
}}
|
||||||
|
onClick={() => setSelectedPlanId(plan.id)}
|
||||||
|
>
|
||||||
|
<h3 style={{ margin: '0 0 6px 0', fontSize: '18px', fontWeight: '600', color: '#1a1a1a' }}>
|
||||||
|
{plan.name}
|
||||||
|
</h3>
|
||||||
|
<div style={{ fontSize: '24px', fontWeight: '700', color: '#0066cc', marginBottom: '6px' }}>
|
||||||
|
${(plan.price / 100).toFixed(2)}/{plan.interval}
|
||||||
|
</div>
|
||||||
|
<p style={{ margin: '0 0 8px 0', fontSize: '14px', color: '#666', lineHeight: '1.4' }}>
|
||||||
|
{plan.description}
|
||||||
|
</p>
|
||||||
|
<ul style={{ margin: 0, paddingLeft: '16px', fontSize: '12px', lineHeight: '1.4' }}>
|
||||||
|
{plan.features.map((feature, index) => (
|
||||||
|
<li key={index} style={{ marginBottom: '2px' }}>{feature}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 'auto', flexShrink: 0, display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '6px', fontWeight: '600', fontSize: '14px', color: '#1a1a1a' }}>
|
||||||
|
Email Address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={customerEmail}
|
||||||
|
onChange={(e) => setCustomerEmail(e.target.value)}
|
||||||
|
placeholder="Enter your email"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px 12px',
|
||||||
|
border: '2px solid #ddd',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '14px',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={initializeSubscription}
|
||||||
|
disabled={isLoading || !customerEmail.trim()}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '14px',
|
||||||
|
backgroundColor: (isLoading || !customerEmail.trim()) ? '#ccc' : '#0066cc',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: '600',
|
||||||
|
cursor: (isLoading || !customerEmail.trim()) ? 'not-allowed' : 'pointer',
|
||||||
|
transition: 'background-color 0.2s ease',
|
||||||
|
minHeight: '44px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Initializing...' : !customerEmail.trim() ? 'Enter email to continue' : `Subscribe to ${SUBSCRIPTION_PLANS.find(plan => plan.id === selectedPlanId)?.name} - $${(SUBSCRIPTION_PLANS.find(plan => plan.id === selectedPlanId)?.price || 0) / 100}/month`}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<StripePaymentForm clientSecret={clientSecret} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)',
|
||||||
|
backgroundColor: '#ffffff', padding: '32px', borderRadius: '12px',
|
||||||
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.2)', textAlign: 'center', zIndex: 2,
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '40px', color: '#dc3545', marginBottom: '12px' }}>⚠️</div>
|
||||||
|
<div style={{ color: '#dc3545', fontSize: '16px', fontWeight: '600', marginBottom: '20px' }}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => { setError(null); setClientSecret(null); initializeSubscription(); }}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px', backgroundColor: '#0066cc', color: 'white',
|
||||||
|
border: 'none', borderRadius: '6px', cursor: 'pointer',
|
||||||
|
fontSize: '14px', fontWeight: '600', minHeight: '40px', minWidth: '100px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main shape utility class
|
||||||
|
export class StripePaymentShapeUtil extends BaseBoxShapeUtil<StripePaymentShape> {
|
||||||
|
static type = 'stripe-payment' as const;
|
||||||
|
|
||||||
|
getDefaultProps(): StripePaymentShape['props'] {
|
||||||
|
return {
|
||||||
|
w: 400,
|
||||||
|
h: 200,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
override canEdit() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
override canResize() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
override onResize(shape: StripePaymentShape) {
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
component(shape: StripePaymentShape) {
|
||||||
|
const [showPopup, setShowPopup] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HTMLContainer>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: shape.props.w,
|
||||||
|
height: shape.props.h,
|
||||||
|
padding: '20px',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
border: '2px solid #e0e0e0',
|
||||||
|
borderRadius: '12px',
|
||||||
|
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '48px', marginBottom: '16px', color: '#0066cc' }}>💳</div>
|
||||||
|
<h3 style={{ margin: '0 0 8px 0', fontSize: '20px', fontWeight: '600', color: '#1a1a1a', textAlign: 'center' }}>
|
||||||
|
Stripe Payment
|
||||||
|
</h3>
|
||||||
|
<p style={{ margin: '0 0 8px 0', fontSize: '14px', color: '#666', textAlign: 'center', lineHeight: '1.4' }}>
|
||||||
|
Click the button below to start your subscription
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPopup(true)}
|
||||||
|
style={{
|
||||||
|
padding: '12px 24px',
|
||||||
|
backgroundColor: '#0066cc',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: '600',
|
||||||
|
boxShadow: '0 4px 8px rgba(0, 102, 204, 0.2)',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = '#0052a3';
|
||||||
|
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = '#0066cc';
|
||||||
|
e.currentTarget.style.transform = 'translateY(0)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Subscribe with Credit Card
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{showPopup && <StripePaymentPopup onClose={() => setShowPopup(false)} />}
|
||||||
|
</HTMLContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator(shape: StripePaymentShape) {
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
width={shape.props.w}
|
||||||
|
height={shape.props.h}
|
||||||
|
rx={8}
|
||||||
|
ry={8}
|
||||||
|
fill="none"
|
||||||
|
stroke="#0066cc"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { BaseBoxShapeTool } from "tldraw"
|
||||||
|
|
||||||
|
export class AgentTool extends BaseBoxShapeTool {
|
||||||
|
static override id = "Agent"
|
||||||
|
shapeType = "Agent"
|
||||||
|
override initial = "idle"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { BaseBoxShapeTool } from "tldraw"
|
||||||
|
|
||||||
|
export class StripePaymentTool extends BaseBoxShapeTool {
|
||||||
|
static override id = "stripe-payment"
|
||||||
|
shapeType = "stripe-payment"
|
||||||
|
override initial = "idle"
|
||||||
|
}
|
||||||
|
|
@ -100,6 +100,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||||
<TldrawUiMenuItem {...customActions.unlockElement} disabled={!hasSelection} />
|
<TldrawUiMenuItem {...customActions.unlockElement} disabled={!hasSelection} />
|
||||||
<TldrawUiMenuItem {...customActions.saveToPdf} disabled={!hasSelection} />
|
<TldrawUiMenuItem {...customActions.saveToPdf} disabled={!hasSelection} />
|
||||||
<TldrawUiMenuItem {...customActions.llm} disabled={!hasSelection} />
|
<TldrawUiMenuItem {...customActions.llm} disabled={!hasSelection} />
|
||||||
|
<TldrawUiMenuItem {...customActions.createStripePayment} />
|
||||||
</TldrawUiMenuGroup>
|
</TldrawUiMenuGroup>
|
||||||
|
|
||||||
{/* Creation Tools Group */}
|
{/* Creation Tools Group */}
|
||||||
|
|
@ -111,6 +112,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||||
<TldrawUiMenuItem {...tools.Markdown} disabled={hasSelection} />
|
<TldrawUiMenuItem {...tools.Markdown} disabled={hasSelection} />
|
||||||
<TldrawUiMenuItem {...tools.MycrozineTemplate} disabled={hasSelection} />
|
<TldrawUiMenuItem {...tools.MycrozineTemplate} disabled={hasSelection} />
|
||||||
<TldrawUiMenuItem {...tools.Prompt} disabled={hasSelection} />
|
<TldrawUiMenuItem {...tools.Prompt} disabled={hasSelection} />
|
||||||
|
<TldrawUiMenuItem {...tools.StripePayment} disabled={hasSelection} />
|
||||||
</TldrawUiMenuGroup>
|
</TldrawUiMenuGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,14 @@ export function CustomToolbar() {
|
||||||
isSelected={tools["Prompt"].id === editor.getCurrentToolId()}
|
isSelected={tools["Prompt"].id === editor.getCurrentToolId()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{tools["stripe-payment"] && (
|
||||||
|
<TldrawUiMenuItem
|
||||||
|
{...tools["stripe-payment"]}
|
||||||
|
icon="credit-card"
|
||||||
|
label="Stripe Subscription"
|
||||||
|
isSelected={tools["stripe-payment"].id === editor.getCurrentToolId()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DefaultToolbar>
|
</DefaultToolbar>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,15 @@ export const overrides: TLUiOverrides = {
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect: () => editor.setCurrentTool("Prompt"),
|
onSelect: () => editor.setCurrentTool("Prompt"),
|
||||||
},
|
},
|
||||||
|
StripePayment: {
|
||||||
|
id: "stripe-payment",
|
||||||
|
icon: "credit-card",
|
||||||
|
label: "Stripe Subscription",
|
||||||
|
type: "stripe-payment",
|
||||||
|
kbd: "alt+shift+p",
|
||||||
|
readonlyOk: true,
|
||||||
|
onSelect: () => editor.setCurrentTool("stripe-payment"),
|
||||||
|
},
|
||||||
hand: {
|
hand: {
|
||||||
...tools.hand,
|
...tools.hand,
|
||||||
onDoubleClick: (info: any) => {
|
onDoubleClick: (info: any) => {
|
||||||
|
|
@ -348,6 +357,15 @@ export const overrides: TLUiOverrides = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
createStripePayment: {
|
||||||
|
id: "create-stripe-payment",
|
||||||
|
label: "Create Stripe Subscription",
|
||||||
|
kbd: "alt+shift+s",
|
||||||
|
readonlyOk: true,
|
||||||
|
onSelect: () => {
|
||||||
|
editor.setCurrentTool("stripe-payment")
|
||||||
|
},
|
||||||
|
},
|
||||||
//TODO: FIX PREV & NEXT SLIDE KEYBOARD COMMANDS
|
//TODO: FIX PREV & NEXT SLIDE KEYBOARD COMMANDS
|
||||||
// "next-slide": {
|
// "next-slide": {
|
||||||
// id: "next-slide",
|
// id: "next-slide",
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ export default defineConfig(({ mode }) => {
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
__WORKER_URL__: JSON.stringify(env.VITE_TLDRAW_WORKER_URL),
|
__WORKER_URL__: JSON.stringify(env.VITE_TLDRAW_WORKER_URL),
|
||||||
__DAILY_API_KEY__: JSON.stringify(env.VITE_DAILY_API_KEY)
|
__DAILY_API_KEY__: JSON.stringify(env.VITE_DAILY_API_KEY),
|
||||||
|
__STRIPE_PUBLISHABLE_KEY__: JSON.stringify(env.VITE_STRIPE_PUBLISHABLE_KEY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { MarkdownShape } from "@/shapes/MarkdownShapeUtil"
|
||||||
import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil"
|
import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil"
|
||||||
import { SlideShape } from "@/shapes/SlideShapeUtil"
|
import { SlideShape } from "@/shapes/SlideShapeUtil"
|
||||||
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
||||||
|
import { StripePaymentShapeUtil } from "@/shapes/stripe/StripePaymentShapeUtil"
|
||||||
|
|
||||||
// add custom shapes and bindings here if needed:
|
// add custom shapes and bindings here if needed:
|
||||||
export const customSchema = createTLSchema({
|
export const customSchema = createTLSchema({
|
||||||
|
|
@ -52,6 +53,9 @@ export const customSchema = createTLSchema({
|
||||||
props: PromptShape.props,
|
props: PromptShape.props,
|
||||||
migrations: PromptShape.migrations,
|
migrations: PromptShape.migrations,
|
||||||
},
|
},
|
||||||
|
'stripe-payment': {
|
||||||
|
props: StripePaymentShapeUtil.props,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
bindings: defaultBindingSchemas,
|
bindings: defaultBindingSchemas,
|
||||||
})
|
})
|
||||||
|
|
@ -166,20 +170,55 @@ export class TldrawDurableObject {
|
||||||
serverWebSocket.accept()
|
serverWebSocket.accept()
|
||||||
const room = await this.getRoom()
|
const room = await this.getRoom()
|
||||||
|
|
||||||
|
// Add connection state tracking
|
||||||
|
let isConnected = true
|
||||||
|
|
||||||
// Handle socket connection with proper error boundaries
|
// Handle socket connection with proper error boundaries
|
||||||
room.handleSocketConnect({
|
room.handleSocketConnect({
|
||||||
sessionId,
|
sessionId,
|
||||||
socket: {
|
socket: {
|
||||||
send: serverWebSocket.send.bind(serverWebSocket),
|
send: (message) => {
|
||||||
close: serverWebSocket.close.bind(serverWebSocket),
|
if (isConnected && serverWebSocket.readyState === WebSocket.OPEN) {
|
||||||
addEventListener:
|
try {
|
||||||
serverWebSocket.addEventListener.bind(serverWebSocket),
|
serverWebSocket.send(message)
|
||||||
removeEventListener:
|
} catch (error) {
|
||||||
serverWebSocket.removeEventListener.bind(serverWebSocket),
|
console.error("WebSocket send error:", error)
|
||||||
|
isConnected = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close: (code, reason) => {
|
||||||
|
if (isConnected) {
|
||||||
|
try {
|
||||||
|
serverWebSocket.close(code, reason)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("WebSocket close error:", error)
|
||||||
|
} finally {
|
||||||
|
isConnected = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addEventListener: (event, listener) => {
|
||||||
|
serverWebSocket.addEventListener(event, listener)
|
||||||
|
},
|
||||||
|
removeEventListener: (event, listener) => {
|
||||||
|
serverWebSocket.removeEventListener(event, listener)
|
||||||
|
},
|
||||||
readyState: serverWebSocket.readyState,
|
readyState: serverWebSocket.readyState,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add WebSocket event listeners for better error handling
|
||||||
|
serverWebSocket.addEventListener("error", (event) => {
|
||||||
|
console.error("WebSocket error:", event)
|
||||||
|
isConnected = false
|
||||||
|
})
|
||||||
|
|
||||||
|
serverWebSocket.addEventListener("close", (event) => {
|
||||||
|
console.log("WebSocket closed:", event.code, event.reason)
|
||||||
|
isConnected = false
|
||||||
|
})
|
||||||
|
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 101,
|
status: 101,
|
||||||
webSocket: clientWebSocket,
|
webSocket: clientWebSocket,
|
||||||
|
|
@ -194,7 +233,11 @@ export class TldrawDurableObject {
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("WebSocket connection error:", error)
|
console.error("WebSocket connection error:", error)
|
||||||
serverWebSocket.close(1011, "Failed to initialize connection")
|
try {
|
||||||
|
serverWebSocket.close(1011, "Failed to initialize connection")
|
||||||
|
} catch (closeError) {
|
||||||
|
console.error("Error closing WebSocket:", closeError)
|
||||||
|
}
|
||||||
return new Response("Failed to establish WebSocket connection", {
|
return new Response("Failed to establish WebSocket connection", {
|
||||||
status: 500,
|
status: 500,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,6 @@ export interface Environment {
|
||||||
TLDRAW_DURABLE_OBJECT: DurableObjectNamespace
|
TLDRAW_DURABLE_OBJECT: DurableObjectNamespace
|
||||||
DAILY_API_KEY: string;
|
DAILY_API_KEY: string;
|
||||||
DAILY_DOMAIN: string;
|
DAILY_DOMAIN: string;
|
||||||
|
STRIPE_SECRET_KEY: string;
|
||||||
|
STRIPE_WEBHOOK_SECRET: string;
|
||||||
}
|
}
|
||||||
223
worker/worker.ts
223
worker/worker.ts
|
|
@ -2,10 +2,17 @@ import { handleUnfurlRequest } from "cloudflare-workers-unfurl"
|
||||||
import { AutoRouter, cors, error, IRequest } from "itty-router"
|
import { AutoRouter, cors, error, IRequest } from "itty-router"
|
||||||
import { handleAssetDownload, handleAssetUpload } from "./assetUploads"
|
import { handleAssetDownload, handleAssetUpload } from "./assetUploads"
|
||||||
import { Environment } from "./types"
|
import { Environment } from "./types"
|
||||||
|
import Stripe from "stripe"
|
||||||
|
|
||||||
// make sure our sync durable object is made available to cloudflare
|
// make sure our sync durable object is made available to cloudflare
|
||||||
export { TldrawDurableObject } from "./TldrawDurableObject"
|
export { TldrawDurableObject } from "./TldrawDurableObject"
|
||||||
|
|
||||||
|
// Helper function to get price amount
|
||||||
|
async function getPriceAmount(priceId: string, stripe: Stripe): Promise<number> {
|
||||||
|
const price = await stripe.prices.retrieve(priceId);
|
||||||
|
return price.unit_amount || 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Define security headers
|
// Define security headers
|
||||||
const securityHeaders = {
|
const securityHeaders = {
|
||||||
"Content-Security-Policy":
|
"Content-Security-Policy":
|
||||||
|
|
@ -325,6 +332,222 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Stripe API routes
|
||||||
|
.post("/api/stripe/create-subscription", async (req, env) => {
|
||||||
|
try {
|
||||||
|
const body = await req.json() as {
|
||||||
|
priceId: string;
|
||||||
|
customerEmail?: string;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize Stripe with your secret key
|
||||||
|
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
|
||||||
|
apiVersion: '2025-05-28.basil',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create or get customer
|
||||||
|
let customer;
|
||||||
|
if (body.customerEmail) {
|
||||||
|
const existingCustomers = await stripe.customers.list({
|
||||||
|
email: body.customerEmail,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingCustomers.data.length > 0) {
|
||||||
|
customer = existingCustomers.data[0];
|
||||||
|
} else {
|
||||||
|
customer = await stripe.customers.create({
|
||||||
|
email: body.customerEmail,
|
||||||
|
metadata: body.metadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
customer = await stripe.customers.create({
|
||||||
|
metadata: body.metadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a payment intent for the first payment
|
||||||
|
const paymentIntent = await stripe.paymentIntents.create({
|
||||||
|
amount: await getPriceAmount(body.priceId, stripe),
|
||||||
|
currency: 'cad',
|
||||||
|
customer: customer.id,
|
||||||
|
description: `Subscription payment for ${body.metadata?.planName || 'plan'}`,
|
||||||
|
metadata: {
|
||||||
|
...body.metadata,
|
||||||
|
price_id: body.priceId,
|
||||||
|
customer_id: customer.id,
|
||||||
|
},
|
||||||
|
automatic_payment_methods: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
setup_future_usage: 'off_session', // Allow future payments for subscription
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Payment intent created:', paymentIntent.id);
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
client_secret: paymentIntent.client_secret,
|
||||||
|
payment_intent_id: paymentIntent.id,
|
||||||
|
customer_id: customer.id,
|
||||||
|
price_id: body.priceId,
|
||||||
|
}), {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Stripe subscription creation error:', error);
|
||||||
|
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.post("/api/stripe/create-payment-intent", async (req: Request, env: Environment) => {
|
||||||
|
try {
|
||||||
|
const body = await req.json() as {
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
description: string;
|
||||||
|
customerEmail?: string;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize Stripe with your secret key
|
||||||
|
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
|
||||||
|
apiVersion: '2025-05-28.basil',
|
||||||
|
});
|
||||||
|
|
||||||
|
const paymentIntent = await stripe.paymentIntents.create({
|
||||||
|
amount: body.amount,
|
||||||
|
currency: body.currency,
|
||||||
|
description: body.description,
|
||||||
|
receipt_email: body.customerEmail,
|
||||||
|
metadata: body.metadata,
|
||||||
|
automatic_payment_methods: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
client_secret: paymentIntent.client_secret,
|
||||||
|
payment_intent_id: paymentIntent.id,
|
||||||
|
}), {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.post("/api/stripe/webhook", async (req: Request, env: Environment) => {
|
||||||
|
const body = await req.text();
|
||||||
|
const signature = req.headers.get('stripe-signature');
|
||||||
|
|
||||||
|
if (!signature) {
|
||||||
|
return new Response('No signature', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
|
||||||
|
apiVersion: '2025-05-28.basil',
|
||||||
|
});
|
||||||
|
|
||||||
|
const event = stripe.webhooks.constructEvent(
|
||||||
|
body,
|
||||||
|
signature,
|
||||||
|
env.STRIPE_WEBHOOK_SECRET
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case 'customer.subscription.created':
|
||||||
|
const subscription = event.data.object;
|
||||||
|
console.log('Subscription created:', subscription.id);
|
||||||
|
await notifySubscriptionCreated(subscription);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'customer.subscription.updated':
|
||||||
|
const updatedSubscription = event.data.object;
|
||||||
|
console.log('Subscription updated:', updatedSubscription.id);
|
||||||
|
await notifySubscriptionUpdated(updatedSubscription);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'customer.subscription.deleted':
|
||||||
|
const deletedSubscription = event.data.object;
|
||||||
|
console.log('Subscription deleted:', deletedSubscription.id);
|
||||||
|
await notifySubscriptionDeleted(deletedSubscription);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'invoice.payment_succeeded':
|
||||||
|
const invoice = event.data.object;
|
||||||
|
console.log('Invoice payment succeeded:', invoice.id);
|
||||||
|
await notifyInvoicePaymentSucceeded(invoice);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'invoice.payment_failed':
|
||||||
|
const failedInvoice = event.data.object;
|
||||||
|
console.log('Invoice payment failed:', failedInvoice.id);
|
||||||
|
await notifyInvoicePaymentFailed(failedInvoice);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'payment_intent.succeeded':
|
||||||
|
const paymentIntent = event.data.object;
|
||||||
|
console.log('Payment succeeded:', paymentIntent.id);
|
||||||
|
await notifyPaymentSuccess(paymentIntent);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'payment_intent.payment_failed':
|
||||||
|
const failedPayment = event.data.object;
|
||||||
|
console.log('Payment failed:', failedPayment.id);
|
||||||
|
await notifyPaymentFailure(failedPayment);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`Unhandled event type: ${event.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('Webhook handled', { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Webhook error:', error);
|
||||||
|
return new Response('Webhook error', { status: 400 });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function notifySubscriptionCreated(subscription: any) {
|
||||||
|
console.log('Subscription created notification:', subscription.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function notifySubscriptionUpdated(subscription: any) {
|
||||||
|
console.log('Subscription updated notification:', subscription.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function notifySubscriptionDeleted(subscription: any) {
|
||||||
|
console.log('Subscription deleted notification:', subscription.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function notifyInvoicePaymentSucceeded(invoice: any) {
|
||||||
|
console.log('Invoice payment succeeded notification:', invoice.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function notifyInvoicePaymentFailed(invoice: any) {
|
||||||
|
console.log('Invoice payment failed notification:', invoice.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function notifyPaymentSuccess(paymentIntent: any) {
|
||||||
|
// Implementation depends on your notification system
|
||||||
|
// Could update database, send email, or trigger real-time updates
|
||||||
|
console.log('Payment success notification:', paymentIntent.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function notifyPaymentFailure(paymentIntent: any) {
|
||||||
|
// Implementation depends on your notification system
|
||||||
|
console.log('Payment failure notification:', paymentIntent.id);
|
||||||
|
}
|
||||||
|
|
||||||
async function backupAllBoards(env: Environment) {
|
async function backupAllBoards(env: Environment) {
|
||||||
try {
|
try {
|
||||||
// List all room files from TLDRAW_BUCKET
|
// List all room files from TLDRAW_BUCKET
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ account_id = "0e7b3338d5278ed1b148e6456b940913"
|
||||||
# Workers & Pages → jeffemmett-canvas → Settings → Variables
|
# Workers & Pages → jeffemmett-canvas → Settings → Variables
|
||||||
DAILY_DOMAIN = "mycopunks.daily.co"
|
DAILY_DOMAIN = "mycopunks.daily.co"
|
||||||
|
|
||||||
|
# Stripe configuration
|
||||||
|
# These should be set as secrets using `wrangler secret put STRIPE_SECRET_KEY` and `wrangler secret put STRIPE_WEBHOOK_SECRET`
|
||||||
|
|
||||||
[dev]
|
[dev]
|
||||||
port = 5172
|
port = 5172
|
||||||
ip = "0.0.0.0"
|
ip = "0.0.0.0"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue