feat: 添加订阅购买API功能

This commit is contained in:
zyh 2024-10-22 08:29:14 +00:00
parent b0bae22ad0
commit 7c0932e7b5
13 changed files with 498 additions and 120 deletions

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,87 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogRoot } from '~/components/ui/Dialog';
import { toast } from 'react-toastify';
interface PaymentModalProps {
isOpen: boolean;
onClose: () => void;
paymentData: PaymentResponse;
onPaymentSuccess: () => void;
}
interface PaymentResponse {
status: string;
msg: string;
no: string;
pay_type: string;
order_amount: string;
pay_amount: string;
qr_money: string;
qr: string;
qr_img: string;
did: string;
expires_in: string;
return_url: string;
}
export function PaymentModal({ isOpen, onClose, paymentData, onPaymentSuccess }: PaymentModalProps) {
const [timeLeft, setTimeLeft] = useState(parseInt(paymentData.expires_in));
const checkPaymentStatus = useCallback(async () => {
try {
const response = await fetch(`/api/check-payment-status?orderNo=${paymentData.no}`);
const data = await response.json();
if (data.status === 'completed') {
clearInterval(timer);
onPaymentSuccess();
onClose();
toast.success('支付成功!');
}
} catch (error) {
console.error('Error checking payment status:', error);
}
}, [paymentData.no, onPaymentSuccess, onClose]);
useEffect(() => {
if (!isOpen) return;
const timer = setInterval(() => {
setTimeLeft((prevTime) => {
if (prevTime <= 1) {
clearInterval(timer);
onClose();
toast.error('支付超时,请重新发起支付');
return 0;
}
return prevTime - 1;
});
checkPaymentStatus();
}, 3000); // 每3秒检查一次支付状态
return () => clearInterval(timer);
}, [isOpen, onClose, checkPaymentStatus]);
return (
<DialogRoot open={isOpen}>
<Dialog onBackdrop={onClose} onClose={onClose} className="w-full max-w-md">
<DialogTitle></DialogTitle>
<DialogDescription>
<div className="space-y-4">
<div className="text-center">
<img src={paymentData.qr_img} alt="支付二维码" className="mx-auto" />
</div>
<div className="text-center">
<p className="text-bolt-elements-textPrimary">: ¥{paymentData.order_amount}</p>
<p className="text-bolt-elements-textSecondary">: {paymentData.no}</p>
<p className="text-bolt-elements-textSecondary">: {paymentData.pay_type}</p>
</div>
<div className="text-center">
<p className="text-bolt-elements-textPrimary">: {timeLeft}</p>
</div>
</div>
</DialogDescription>
</Dialog>
</DialogRoot>
);
}

View File

@ -1,7 +1,8 @@
import { Dialog, DialogTitle, DialogDescription, DialogRoot } from '~/components/ui/Dialog';
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { useAuth } from '~/hooks/useAuth';
import { toast } from 'react-toastify';
import { PaymentModal } from './PaymentModal';
interface SubscriptionDialogProps {
isOpen: boolean;
@ -23,12 +24,28 @@ interface UserSubscription {
nextReloadDate: string;
}
interface PaymentResponse {
status: string;
msg: string;
no: string;
pay_type: string;
order_amount: string;
pay_amount: string;
qr_money: string;
qr: string;
qr_img: string;
did: string;
expires_in: string;
return_url: string;
}
export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps) {
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
const [subscriptionPlans, setSubscriptionPlans] = useState<SubscriptionPlan[]>([]);
const [userSubscription, setUserSubscription] = useState<UserSubscription | null>(null);
const [isLoading, setIsLoading] = useState(true);
const { user } = useAuth();
const [paymentData, setPaymentData] = useState<PaymentResponse | null>(null);
useEffect(() => {
if (isOpen && user) {
@ -68,107 +85,121 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
}),
});
const result = await response.json();
if (response.ok) {
toast.success('订阅购买成功!');
fetchSubscriptionData(); // 刷新订阅信息
if (response.ok && result.paymentData) {
setPaymentData(result.paymentData);
} else {
toast.error(result.message || '购买失败,请稍后重试。');
toast.error(result.message || '获取支付信息失败,请稍后重试。');
}
} catch (error) {
console.error('Error purchasing subscription:', error);
console.error('Error initiating purchase:', error);
toast.error('购买过程中出现错误,请稍后重试。');
}
};
const handlePaymentSuccess = useCallback(() => {
fetchSubscriptionData(); // 重新获取订阅信息
toast.success('订阅成功!');
}, [fetchSubscriptionData]);
if (!user || isLoading) return null;
return (
<DialogRoot open={isOpen}>
<Dialog onBackdrop={onClose} onClose={onClose} className="w-full max-w-4xl">
<DialogTitle></DialogTitle>
<DialogDescription>
<div className="space-y-6">
<div className="text-center">
<p className="text-bolt-elements-textSecondary">
</p>
</div>
{userSubscription && (
<div className="bg-bolt-elements-background-depth-2 p-4 rounded-lg">
<div className="flex justify-between items-center">
<div>
<span className="text-bolt-elements-textPrimary font-bold">{userSubscription.tokensLeft.toLocaleString()}</span>
<span className="text-bolt-elements-textSecondary"> </span>
<span className="text-bolt-elements-textSecondary">
{userSubscription.plan.tokens.toLocaleString()}{new Date(userSubscription.nextReloadDate).toLocaleDateString()}
</span>
</div>
<div className="text-right">
<span className="text-bolt-elements-textSecondary"></span>
<br />
<span className="text-bolt-elements-textSecondary">
<a href="#" className="text-bolt-elements-item-contentAccent hover:underline"></a>
</span>
</div>
</div>
<>
<DialogRoot open={isOpen}>
<Dialog onBackdrop={onClose} onClose={onClose} className="w-full max-w-4xl">
<DialogTitle></DialogTitle>
<DialogDescription>
<div className="space-y-6">
<div className="text-center">
<p className="text-bolt-elements-textSecondary">
</p>
</div>
)}
<div className="flex justify-center space-x-4">
<button
onClick={() => setBillingCycle('monthly')}
className={`px-4 py-2 rounded-md ${
billingCycle === 'monthly'
? 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text'
: 'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text'
}`}
>
</button>
<button
onClick={() => setBillingCycle('yearly')}
className={`px-4 py-2 rounded-md ${
billingCycle === 'yearly'
? 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text'
: 'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text'
}`}
>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{subscriptionPlans.map((plan) => (
<div key={plan._id} className={`bg-bolt-elements-background-depth-2 p-4 rounded-lg ${plan._id === userSubscription?.plan._id ? 'border-2 border-bolt-elements-item-contentAccent' : ''}`}>
<h3 className="text-bolt-elements-textPrimary font-bold text-lg">{plan.name}</h3>
<div className="text-bolt-elements-textSecondary mb-2">
{(plan.tokens / 1000000).toFixed(0)}M
{plan.save_percentage && (
<span className="ml-2 text-green-500"> {plan.save_percentage}%</span>
)}
{userSubscription && (
<div className="bg-bolt-elements-background-depth-2 p-4 rounded-lg">
<div className="flex justify-between items-center">
<div>
<span className="text-bolt-elements-textPrimary font-bold">{userSubscription.tokensLeft.toLocaleString()}</span>
<span className="text-bolt-elements-textSecondary"> </span>
<span className="text-bolt-elements-textSecondary">
{userSubscription.plan.tokens.toLocaleString()}{new Date(userSubscription.nextReloadDate).toLocaleDateString()}
</span>
</div>
<div className="text-right">
<span className="text-bolt-elements-textSecondary"></span>
<br />
<span className="text-bolt-elements-textSecondary">
<a href="#" className="text-bolt-elements-item-contentAccent hover:underline"></a>
</span>
</div>
</div>
<p className="text-bolt-elements-textTertiary text-sm mb-4">{plan.description}</p>
<div className="text-bolt-elements-textPrimary font-bold text-2xl mb-2">
¥{plan.price * (billingCycle === 'yearly' ? 10 : 1)}/{billingCycle === 'yearly' ? '年' : '月'}
</div>
<button
onClick={() => handlePurchase(plan._id)}
className={`w-full py-2 rounded-md ${
plan._id === userSubscription?.plan._id
? 'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text'
: 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text'
}`}
>
{plan._id === userSubscription?.plan._id ? '管理当前计划' : `升级到${plan.name}`}
</button>
</div>
))}
)}
<div className="flex justify-center space-x-4">
<button
onClick={() => setBillingCycle('monthly')}
className={`px-4 py-2 rounded-md ${
billingCycle === 'monthly'
? 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text'
: 'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text'
}`}
>
</button>
<button
onClick={() => setBillingCycle('yearly')}
className={`px-4 py-2 rounded-md ${
billingCycle === 'yearly'
? 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text'
: 'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text'
}`}
>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{subscriptionPlans.map((plan) => (
<div key={plan._id} className={`bg-bolt-elements-background-depth-2 p-4 rounded-lg ${plan._id === userSubscription?.plan._id ? 'border-2 border-bolt-elements-item-contentAccent' : ''}`}>
<h3 className="text-bolt-elements-textPrimary font-bold text-lg">{plan.name}</h3>
<div className="text-bolt-elements-textSecondary mb-2">
{(plan.tokens / 1000000).toFixed(0)}M
{plan.save_percentage && (
<span className="ml-2 text-green-500"> {plan.save_percentage}%</span>
)}
</div>
<p className="text-bolt-elements-textTertiary text-sm mb-4">{plan.description}</p>
<div className="text-bolt-elements-textPrimary font-bold text-2xl mb-2">
¥{plan.price * (billingCycle === 'yearly' ? 10 : 1)}/{billingCycle === 'yearly' ? '年' : '月'}
</div>
<button
onClick={() => handlePurchase(plan._id)}
className={`w-full py-2 rounded-md ${
plan._id === userSubscription?.plan._id
? 'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text'
: 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text'
}`}
>
{plan._id === userSubscription?.plan._id ? '管理当前计划' : `升级到${plan.name}`}
</button>
</div>
))}
</div>
</div>
</div>
</DialogDescription>
</Dialog>
</DialogRoot>
</DialogDescription>
</Dialog>
</DialogRoot>
{paymentData && (
<PaymentModal
isOpen={!!paymentData}
onClose={() => setPaymentData(null)}
paymentData={paymentData}
onPaymentSuccess={handlePaymentSuccess}
/>
)}
</>
);
}

View File

@ -0,0 +1,26 @@
import { json } from '@remix-run/cloudflare';
import { db } from '~/utils/db.server';
export async function loader({ request }: { request: Request }) {
const url = new URL(request.url);
const orderNo = url.searchParams.get('orderNo');
if (!orderNo) {
return json({ error: 'Order number is required' }, { status: 400 });
}
try {
const transaction = await db('user_transactions')
.where('transaction_id', orderNo)
.first();
if (!transaction) {
return json({ error: 'Transaction not found' }, { status: 404 });
}
return json({ status: transaction.status });
} catch (error) {
console.error('Error checking payment status:', error);
return json({ error: 'Failed to check payment status' }, { status: 500 });
}
}

View File

@ -0,0 +1,45 @@
import { json } from '@remix-run/cloudflare';
import { db } from '~/utils/db.server';
import SDPay, { type SDNotifyBody } from '~/utils/SDPay.server';
export async function action({ request }: { request: Request }) {
const formData = await request.formData();
const notifyParams = Object.fromEntries(formData) as unknown as SDNotifyBody;
const sdpay = new SDPay();
if (!sdpay.verifyNotify(notifyParams)) {
return json({ error: 'Invalid signature' }, { status: 400 });
}
try {
await db.transaction(async (trx) => {
// 更新交易状态
await trx('user_transactions')
.where('transaction_id', notifyParams.order_no)
.update({
status: 'completed',
_update: db.fn.now(),
});
// 获取交易详情
const transaction = await trx('user_transactions')
.where('transaction_id', notifyParams.order_no)
.first();
if (!transaction) {
throw new Error('Transaction not found');
}
// 更新用户的代币余额
await trx('users')
.where('_id', transaction.user_id)
.increment('token_balance', transaction.tokens);
});
return json({ success: true });
} catch (error) {
console.error('Error processing payment notification:', error);
return json({ error: 'Failed to process payment notification' }, { status: 500 });
}
}

View File

@ -1,42 +1,53 @@
import { json } from '@remix-run/cloudflare';
import { db } from '~/lib/db.server';
import { requireUserId } from '~/lib/session.server';
import { db } from '~/utils/db.server';
import { requireUserId } from '../utils/session.server'; // 使用相对路径
import SDPay from '~/utils/SDPay.server';
export async function action({ request }) {
export async function action({ request }: { request: Request }) {
const userId = await requireUserId(request);
const { planId, billingCycle } = await request.json();
const { planId, billingCycle } = await request.json() as { planId: string; billingCycle: string };
try {
// 开始数据库事务
await db.transaction(async (trx) => {
// 获取订阅计划详情
const plan = await trx('subscription_plans').where('_id', planId).first();
if (!plan) {
throw new Error('Invalid subscription plan');
}
// 获取订阅计划详情
const plan = await db('subscription_plans').where('_id', planId).first();
if (!plan) {
return json({ error: 'Invalid subscription plan' }, { status: 400 });
}
// 计算实际价格和代币数量
const price = billingCycle === 'yearly' ? plan.price * 10 : plan.price;
const tokens = billingCycle === 'yearly' ? plan.tokens * 12 : plan.tokens;
// 计算实际价格和代币数量
const price = billingCycle === 'yearly' ? plan.price * 10 : plan.price;
const tokens = billingCycle === 'yearly' ? plan.tokens * 12 : plan.tokens;
// 创建交易记录
await trx('user_transactions').insert({
user_id: userId,
type: 'subscription',
plan_id: planId,
amount: price,
tokens: tokens,
status: 'completed', // 假设支付已完成
payment_method: 'credit_card', // 假设使用信用卡支付
transaction_id: `sub_${Date.now()}`, // 生成一个简单的交易ID
});
// 创建 SDPay 实例
const sdpay = new SDPay();
// 这里可以添加更多逻辑,如更新用户的订阅状态等
// 生成订单号
const orderNo = `sub_${Date.now()}_${userId}`;
// 获取支付数据
const paymentData = await sdpay.createPayment(
orderNo,
`${plan.name} 订阅 (${billingCycle === 'yearly' ? '年付' : '月付'})`,
'alipay', // 或其他支付方式
price * 100, // 转换为分
userId.toString()
);
// 创建待处理的交易记录
await db('user_transactions').insert({
user_id: userId,
type: 'subscription',
plan_id: planId,
amount: price,
tokens: tokens,
status: 'pending',
payment_method: 'alipay', // 或其他支付方式
transaction_id: orderNo,
});
return json({ success: true, message: '订阅购买成功' });
return json({ success: true, paymentData });
} catch (error) {
console.error('Error purchasing subscription:', error);
return json({ error: 'Failed to purchase subscription' }, { status: 500 });
console.error('Error initiating subscription purchase:', error);
return json({ error: 'Failed to initiate subscription purchase' }, { status: 500 });
}
}

4
app/types/user.ts Normal file
View File

@ -0,0 +1,4 @@
export interface User {
// ... 其他字段
token_balance: number;
}

123
app/utils/SDPay.server.ts Normal file
View File

@ -0,0 +1,123 @@
import CryptoJS from "crypto-js";
import { toNumber } from "lodash";
import { env } from "node:process";
export interface SDNotifyBody {
no: string;
order_no: string;
trade_name: string;
pay_type: string;
order_amount: string;
pay_amount: string;
order_uid: string;
sign: string;
}
// 将对象转换为表单编码的字符串
function toFormData(obj: any): string {
return Object.keys(obj)
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`)
.join("&");
}
export default class SDPay {
private appId: number;
private apiKey: string;
constructor() {
const appId = env.SDPAY_APP_ID;
const apiKey = env.SDPAY_API_KEY;
console.log(appId, apiKey);
if (!appId || !apiKey) {
throw new Error("SDPAY_APP_ID or SDPAY_API_KEY is not set");
}
this.appId = toNumber(appId);
this.apiKey = apiKey;
}
// 发起付款
public async createPayment(
orderNo: string,
tradeName: string,
payType: string,
orderAmount: number,
orderUid: string,
payerName?: string
): Promise<any> {
const params = {
app_id: this.appId,
order_no: orderNo,
trade_name: tradeName,
pay_type: payType,
order_amount: orderAmount,
order_uid: orderUid,
// payer_name: payerName || "",
} as any;
params["sign"] = this.generateSign(params);
// 将参数转换为表单编码的字符串
const formData = toFormData(params);
const url = `https://api.sdpay.cc/pay?format=json&${formData}`;
console.log(url);
const response = await fetch(url);
if (!response.ok) {
console.log(response.statusText, response.text());
throw new Error("Network response was not ok");
}
return response.json();
}
// 获取支付链接
public async getPaymentLink(
orderNo: string,
tradeName: string,
payType: string,
orderAmount: number,
orderUid: string,
payerName?: string
): Promise<string> {
const params = {
app_id: this.appId,
order_no: orderNo,
trade_name: tradeName,
pay_type: payType,
order_amount: orderAmount,
order_uid: orderUid,
// payer_name: payerName || "",
} as any;
params["sign"] = this.generateSign(params); // 将参数转换为表单编码的字符串
const formData = toFormData(params);
const url = `https://api.sdpay.cc/pay?${formData}`;
return url;
}
// 验证通知
public verifyNotify(params: SDNotifyBody): boolean {
return this.notifySign(params) === params.sign;
}
// 生成签名
private generateSign(params: any): string {
const signString = `app_id=${params.app_id}&order_no=${params.order_no}&trade_name=${params.trade_name}&pay_type=${params.pay_type}&order_amount=${params.order_amount}&order_uid=${params.order_uid}&${this.apiKey}`;
return this.md5(signString);
}
private notifySign(params: SDNotifyBody): string {
//计算签名
// $sign_str = "no=" . $no . "&order_no=" . $order_no . "&trade_name=" . $trade_name . "&pay_type=" . $pay_type . "&order_amount=" . $order_amount . "&pay_amount=" . $pay_amount . "&order_uid=" . $order_uid . "&" . $app_key;
// $sign = strtolower(md5($sign_str));
const signString = `no=${params.no}&order_no=${params.order_no}&trade_name=${params.trade_name}&pay_type=${params.pay_type}&order_amount=${params.order_amount}&pay_amount=${params.pay_amount}&order_uid=${params.order_uid}&${this.apiKey}`;
return this.md5(signString);
}
// MD5加密
private md5(string: string): string {
return CryptoJS.MD5(string).toString();
}
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,11 @@
export function up(knex) {
return knex.schema.table('users', function(table) {
table.bigInteger('token_balance').unsigned().notNullable().defaultTo(0).comment('用户代币余额');
});
}
export function down(knex) {
return knex.schema.table('users', function(table) {
table.dropColumn('token_balance');
});
}

View File

@ -3,7 +3,7 @@ export default {
development: {
client: 'mysql2',
connection: {
host: 'devide.y2o.me',
host: 'localhost',
user: 'd8d_design_ai',
password: 'Kw8aEcm37FwNaCk6',
database: 'd8d_design_ai',

View File

@ -55,6 +55,8 @@
"@remix-run/cloudflare-pages": "^2.10.2",
"@remix-run/node": "^2.13.1",
"@remix-run/react": "^2.10.2",
"@types/crypto-js": "^4.2.2",
"@types/lodash": "^4.17.12",
"@uiw/codemirror-theme-vscode": "^4.23.0",
"@unocss/reset": "^0.61.0",
"@webcontainer/api": "1.3.0-internal.10",
@ -64,6 +66,7 @@
"ai": "^3.3.4",
"ali-oss": "^6.21.0",
"bcryptjs": "^2.4.3",
"crypto-js": "^4.2.0",
"date-fns": "^3.6.0",
"diff": "^5.2.0",
"framer-motion": "^11.2.12",
@ -72,6 +75,7 @@
"jose": "^5.6.3",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"lodash": "^4.17.21",
"mysql2": "^3.11.3",
"nanostores": "^0.10.3",
"react": "^18.2.0",

View File

@ -92,6 +92,12 @@ dependencies:
'@remix-run/react':
specifier: ^2.10.2
version: 2.13.1(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3)
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
'@types/lodash':
specifier: ^4.17.12
version: 4.17.12
'@uiw/codemirror-theme-vscode':
specifier: ^4.23.0
version: 4.23.5(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.1)
@ -119,6 +125,9 @@ dependencies:
bcryptjs:
specifier: ^2.4.3
version: 2.4.3
crypto-js:
specifier: ^4.2.0
version: 4.2.0
date-fns:
specifier: ^3.6.0
version: 3.6.0
@ -143,6 +152,9 @@ dependencies:
knex:
specifier: ^3.1.0
version: 3.1.0(mysql2@3.11.3)
lodash:
specifier: ^4.17.21
version: 4.17.21
mysql2:
specifier: ^3.11.3
version: 3.11.3
@ -181,7 +193,7 @@ dependencies:
version: 0.2.0(@remix-run/react@2.13.1)(@remix-run/server-runtime@2.13.1)(react-dom@18.3.1)(react@18.3.1)
remix-utils:
specifier: ^7.6.0
version: 7.7.0(@remix-run/cloudflare@2.13.1)(@remix-run/node@2.13.1)(@remix-run/react@2.13.1)(react@18.3.1)(zod@3.23.8)
version: 7.7.0(@remix-run/cloudflare@2.13.1)(@remix-run/node@2.13.1)(@remix-run/react@2.13.1)(crypto-js@4.2.0)(react@18.3.1)(zod@3.23.8)
set-cookie-parser:
specifier: 2.4.8
version: 2.4.8
@ -3126,6 +3138,7 @@ packages:
resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==}
cpu: [arm]
os: [linux]
libc: [glibc]
requiresBuild: true
dev: true
optional: true
@ -3134,6 +3147,7 @@ packages:
resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==}
cpu: [arm]
os: [linux]
libc: [musl]
requiresBuild: true
dev: true
optional: true
@ -3142,6 +3156,7 @@ packages:
resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
requiresBuild: true
dev: true
optional: true
@ -3150,6 +3165,7 @@ packages:
resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==}
cpu: [arm64]
os: [linux]
libc: [musl]
requiresBuild: true
dev: true
optional: true
@ -3158,6 +3174,7 @@ packages:
resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
requiresBuild: true
dev: true
optional: true
@ -3166,6 +3183,7 @@ packages:
resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
requiresBuild: true
dev: true
optional: true
@ -3174,6 +3192,7 @@ packages:
resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==}
cpu: [s390x]
os: [linux]
libc: [glibc]
requiresBuild: true
dev: true
optional: true
@ -3182,6 +3201,7 @@ packages:
resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==}
cpu: [x64]
os: [linux]
libc: [glibc]
requiresBuild: true
dev: true
optional: true
@ -3190,6 +3210,7 @@ packages:
resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==}
cpu: [x64]
os: [linux]
libc: [musl]
requiresBuild: true
dev: true
optional: true
@ -3308,6 +3329,10 @@ packages:
/@types/cookie@0.6.0:
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
/@types/crypto-js@4.2.2:
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
dev: false
/@types/debug@4.1.12:
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
dependencies:
@ -3351,6 +3376,10 @@ packages:
'@types/node': 22.7.7
dev: true
/@types/lodash@4.17.12:
resolution: {integrity: sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==}
dev: false
/@types/mdast@3.0.15:
resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==}
dependencies:
@ -4829,6 +4858,10 @@ packages:
randomfill: 1.0.4
dev: true
/crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
dev: false
/css-tree@2.3.1:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
@ -8999,7 +9032,7 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/remix-utils@7.7.0(@remix-run/cloudflare@2.13.1)(@remix-run/node@2.13.1)(@remix-run/react@2.13.1)(react@18.3.1)(zod@3.23.8):
/remix-utils@7.7.0(@remix-run/cloudflare@2.13.1)(@remix-run/node@2.13.1)(@remix-run/react@2.13.1)(crypto-js@4.2.0)(react@18.3.1)(zod@3.23.8):
resolution: {integrity: sha512-J8NhP044nrNIam/xOT1L9a4RQ9FSaA2wyrUwmN8ZT+c/+CdAAf70yfaLnvMyKcV5U+8BcURQ/aVbth77sT6jGA==}
engines: {node: '>=18.0.0'}
peerDependencies:
@ -9035,6 +9068,7 @@ packages:
'@remix-run/cloudflare': 2.13.1(@cloudflare/workers-types@4.20241018.0)(typescript@5.6.3)
'@remix-run/node': 2.13.1(typescript@5.6.3)
'@remix-run/react': 2.13.1(react-dom@18.3.1)(react@18.3.1)(typescript@5.6.3)
crypto-js: 4.2.0
react: 18.3.1
type-fest: 4.26.1
zod: 3.23.8