From 7c0932e7b57a0edef4bf66eb84c116eafcba94c3 Mon Sep 17 00:00:00 2001 From: zyh Date: Tue, 22 Oct 2024 08:29:14 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=AE=A2=E9=98=85?= =?UTF-8?q?=E8=B4=AD=E4=B9=B0API=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/auth/PaymentDialog.tsx | 1 + app/components/auth/PaymentModal.tsx | 87 ++++++++ app/components/auth/SubscriptionDialog.tsx | 203 ++++++++++-------- app/routes/api.check-payment-status.ts | 26 +++ app/routes/api.payment-notify.ts | 45 ++++ app/routes/api.purchase-subscription.ts | 75 ++++--- app/types/user.ts | 4 + app/utils/SDPay.server.ts | 123 +++++++++++ app/utils/session.server.ts | 1 + ...241022160301_add_token_balance_to_users.js | 11 + knexfile.js | 2 +- package.json | 4 + pnpm-lock.yaml | 38 +++- 13 files changed, 499 insertions(+), 121 deletions(-) create mode 100644 app/components/auth/PaymentDialog.tsx create mode 100644 app/components/auth/PaymentModal.tsx create mode 100644 app/routes/api.check-payment-status.ts create mode 100644 app/routes/api.payment-notify.ts create mode 100644 app/types/user.ts create mode 100644 app/utils/SDPay.server.ts create mode 100644 app/utils/session.server.ts create mode 100644 db/migrations/20241022160301_add_token_balance_to_users.js diff --git a/app/components/auth/PaymentDialog.tsx b/app/components/auth/PaymentDialog.tsx new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/app/components/auth/PaymentDialog.tsx @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/components/auth/PaymentModal.tsx b/app/components/auth/PaymentModal.tsx new file mode 100644 index 00000000..3946878f --- /dev/null +++ b/app/components/auth/PaymentModal.tsx @@ -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 ( + + + 请扫码支付 + +
+
+ 支付二维码 +
+
+

订单金额: ¥{paymentData.order_amount}

+

订单号: {paymentData.no}

+

支付方式: {paymentData.pay_type}

+
+
+

剩余支付时间: {timeLeft}秒

+
+
+
+
+
+ ); +} diff --git a/app/components/auth/SubscriptionDialog.tsx b/app/components/auth/SubscriptionDialog.tsx index c467c3dc..bbca1488 100644 --- a/app/components/auth/SubscriptionDialog.tsx +++ b/app/components/auth/SubscriptionDialog.tsx @@ -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([]); const [userSubscription, setUserSubscription] = useState(null); const [isLoading, setIsLoading] = useState(true); const { user } = useAuth(); + const [paymentData, setPaymentData] = useState(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 ( - - - 订阅管理 - -
-
-

- 注册免费账户以加速您在公共项目上的工作流程,或通过即时打开的生产环境提升整个团队的效率。 -

-
+ <> + + + 订阅管理 + +
+
+

+ 注册免费账户以加速您在公共项目上的工作流程,或通过即时打开的生产环境提升整个团队的效率。 +

+
- {userSubscription && ( -
-
-
- {userSubscription.tokensLeft.toLocaleString()} - 代币剩余。 - - {userSubscription.plan.tokens.toLocaleString()}代币将在{new Date(userSubscription.nextReloadDate).toLocaleDateString()}后添加。 - -
-
- 需要更多代币? -
- - 升级您的计划或购买 - 代币充值包 - + {userSubscription && ( +
+
+
+ {userSubscription.tokensLeft.toLocaleString()} + 代币剩余。 + + {userSubscription.plan.tokens.toLocaleString()}代币将在{new Date(userSubscription.nextReloadDate).toLocaleDateString()}后添加。 + +
+
+ 需要更多代币? +
+ + 升级您的计划或购买 + 代币充值包 + +
-
- )} + )} -
- - -
+
+ + +
-
- {subscriptionPlans.map((plan) => ( -
-

{plan.name}

-
- {(plan.tokens / 1000000).toFixed(0)}M 代币 - {plan.save_percentage && ( - 节省 {plan.save_percentage}% - )} +
+ {subscriptionPlans.map((plan) => ( +
+

{plan.name}

+
+ {(plan.tokens / 1000000).toFixed(0)}M 代币 + {plan.save_percentage && ( + 节省 {plan.save_percentage}% + )} +
+

{plan.description}

+
+ ¥{plan.price * (billingCycle === 'yearly' ? 10 : 1)}/{billingCycle === 'yearly' ? '年' : '月'} +
+
-

{plan.description}

-
- ¥{plan.price * (billingCycle === 'yearly' ? 10 : 1)}/{billingCycle === 'yearly' ? '年' : '月'} -
- -
- ))} + ))} +
-
- -
-
+ +
+
+ {paymentData && ( + setPaymentData(null)} + paymentData={paymentData} + onPaymentSuccess={handlePaymentSuccess} + /> + )} + ); } diff --git a/app/routes/api.check-payment-status.ts b/app/routes/api.check-payment-status.ts new file mode 100644 index 00000000..f4ff8619 --- /dev/null +++ b/app/routes/api.check-payment-status.ts @@ -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 }); + } +} diff --git a/app/routes/api.payment-notify.ts b/app/routes/api.payment-notify.ts new file mode 100644 index 00000000..cf6c4681 --- /dev/null +++ b/app/routes/api.payment-notify.ts @@ -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 }); + } +} \ No newline at end of file diff --git a/app/routes/api.purchase-subscription.ts b/app/routes/api.purchase-subscription.ts index 8dd14580..2c0b3763 100644 --- a/app/routes/api.purchase-subscription.ts +++ b/app/routes/api.purchase-subscription.ts @@ -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 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 - }); - - // 这里可以添加更多逻辑,如更新用户的订阅状态等 + // 获取订阅计划详情 + 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; + + // 创建 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 }); } } diff --git a/app/types/user.ts b/app/types/user.ts new file mode 100644 index 00000000..b5708de6 --- /dev/null +++ b/app/types/user.ts @@ -0,0 +1,4 @@ +export interface User { + // ... 其他字段 + token_balance: number; +} diff --git a/app/utils/SDPay.server.ts b/app/utils/SDPay.server.ts new file mode 100644 index 00000000..342ed036 --- /dev/null +++ b/app/utils/SDPay.server.ts @@ -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 { + 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 { + 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(); + } +} diff --git a/app/utils/session.server.ts b/app/utils/session.server.ts new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/app/utils/session.server.ts @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/db/migrations/20241022160301_add_token_balance_to_users.js b/db/migrations/20241022160301_add_token_balance_to_users.js new file mode 100644 index 00000000..35b69a9c --- /dev/null +++ b/db/migrations/20241022160301_add_token_balance_to_users.js @@ -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'); + }); + } \ No newline at end of file diff --git a/knexfile.js b/knexfile.js index fbc10865..aad53919 100644 --- a/knexfile.js +++ b/knexfile.js @@ -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', diff --git a/package.json b/package.json index 4d98a85d..f72b0b72 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8fea0f84..c7dd2158 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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