feat(frontend): append purchase query params and make integration doc bilingual
This commit is contained in:
@@ -1,27 +1,189 @@
|
|||||||
# Sub2API Admin API: Payment Integration
|
# Sub2API Admin API: Payment Integration / 支付集成接口文档
|
||||||
|
|
||||||
This document describes the minimum Admin API surface for external payment systems (for example, sub2apipay) to complete balance top-up and reconciliation.
|
## 中文
|
||||||
|
|
||||||
## Base URL
|
### 概述
|
||||||
|
|
||||||
|
本文档描述外部支付系统(例如 sub2apipay)对接 Sub2API 时的最小 Admin API 集合,用于完成充值发放与对账。
|
||||||
|
|
||||||
|
### 基础地址
|
||||||
|
|
||||||
|
- 生产环境:`https://<your-domain>`
|
||||||
|
- Beta 环境:`http://<your-server-ip>:8084`
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
以下接口均建议使用:
|
||||||
|
|
||||||
|
- 请求头:`x-api-key: admin-<64hex>`(服务间调用推荐)
|
||||||
|
- 请求头:`Content-Type: application/json`
|
||||||
|
|
||||||
|
说明:管理员 JWT 也可访问 admin 路由,但机器对机器调用建议使用 Admin API Key。
|
||||||
|
|
||||||
|
### 1) 一步完成:创建兑换码并兑换
|
||||||
|
|
||||||
|
`POST /api/v1/admin/redeem-codes/create-and-redeem`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 原子化完成“创建固定兑换码 + 兑换给指定用户”。
|
||||||
|
- 常用于支付回调成功后的自动充值。
|
||||||
|
|
||||||
|
必需请求头:
|
||||||
|
|
||||||
|
- `x-api-key`
|
||||||
|
- `Idempotency-Key`
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "s2p_cm1234567890",
|
||||||
|
"type": "balance",
|
||||||
|
"value": 100.0,
|
||||||
|
"user_id": 123,
|
||||||
|
"notes": "sub2apipay order: cm1234567890"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
规则:
|
||||||
|
|
||||||
|
- `code`:外部订单映射的确定性兑换码。
|
||||||
|
- `type`:当前推荐使用 `balance`。
|
||||||
|
- `value`:必须大于 0。
|
||||||
|
- `user_id`:目标用户 ID。
|
||||||
|
|
||||||
|
幂等语义:
|
||||||
|
|
||||||
|
- 同一 `code` 且 `used_by` 一致:返回 `200`(幂等回放)。
|
||||||
|
- 同一 `code` 但 `used_by` 不一致:返回 `409`(冲突)。
|
||||||
|
- 缺少 `Idempotency-Key`:返回 `400`(`IDEMPOTENCY_KEY_REQUIRED`)。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \
|
||||||
|
-H "x-api-key: ${KEY}" \
|
||||||
|
-H "Idempotency-Key: pay-cm1234567890-success" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"code":"s2p_cm1234567890",
|
||||||
|
"type":"balance",
|
||||||
|
"value":100.00,
|
||||||
|
"user_id":123,
|
||||||
|
"notes":"sub2apipay order: cm1234567890"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) 查询用户(可选前置检查)
|
||||||
|
|
||||||
|
`GET /api/v1/admin/users/:id`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 支付成功后充值前,确认目标用户是否存在。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s "${BASE}/api/v1/admin/users/123" \
|
||||||
|
-H "x-api-key: ${KEY}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3) 余额调整(已存在接口)
|
||||||
|
|
||||||
|
`POST /api/v1/admin/users/:id/balance`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 复用现有管理员接口做人工纠偏。
|
||||||
|
- 支持 `set`、`add`、`subtract`。
|
||||||
|
|
||||||
|
示例(扣减):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"balance": 100.0,
|
||||||
|
"operation": "subtract",
|
||||||
|
"notes": "manual correction"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "${BASE}/api/v1/admin/users/123/balance" \
|
||||||
|
-H "x-api-key: ${KEY}" \
|
||||||
|
-H "Idempotency-Key: balance-subtract-cm1234567890" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"balance":100.00,
|
||||||
|
"operation":"subtract",
|
||||||
|
"notes":"manual correction"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4) 购买页跳转 URL Query 参数(iframe 与新窗口统一)
|
||||||
|
|
||||||
|
Sub2API 前端在打开 `purchase_subscription_url` 时,会给 iframe 和“新窗口打开”统一追加 query 参数,确保外部支付页拿到一致上下文。
|
||||||
|
|
||||||
|
追加参数:
|
||||||
|
|
||||||
|
- `user_id`:当前登录用户 ID
|
||||||
|
- `token`:当前登录 JWT token
|
||||||
|
- `theme`:当前主题(`light` / `dark`)
|
||||||
|
- `ui_mode`:当前 UI 模式(固定 `embedded`)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://pay.example.com/pay?user_id=123&token=<jwt>&theme=light&ui_mode=embedded
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5) 失败处理建议
|
||||||
|
|
||||||
|
- 支付状态与充值状态分开落库。
|
||||||
|
- 收到并验证支付回调后,立即标记“支付成功”。
|
||||||
|
- 支付成功但充值失败的订单应允许后续重试。
|
||||||
|
- 重试时继续使用同一 `code`,并使用新的 `Idempotency-Key`。
|
||||||
|
|
||||||
|
### 6) `doc_url` 配置建议
|
||||||
|
|
||||||
|
Sub2API 已支持系统设置中的 `doc_url` 字段。
|
||||||
|
|
||||||
|
推荐配置:
|
||||||
|
|
||||||
|
- 查看链接:`https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md`
|
||||||
|
- 下载链接:`https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/ADMIN_PAYMENT_INTEGRATION_API.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
This document defines the minimum Admin API surface for integrating external payment systems (for example, sub2apipay) with Sub2API for recharge fulfillment and reconciliation.
|
||||||
|
|
||||||
|
### Base URL
|
||||||
|
|
||||||
- Production: `https://<your-domain>`
|
- Production: `https://<your-domain>`
|
||||||
- Beta: `http://<your-server-ip>:8084`
|
- Beta: `http://<your-server-ip>:8084`
|
||||||
|
|
||||||
All endpoints below use:
|
### Authentication
|
||||||
|
|
||||||
- Header: `x-api-key: admin-<64hex>` (recommended for server-to-server)
|
Recommended headers:
|
||||||
- Header: `Content-Type: application/json`
|
|
||||||
|
|
||||||
Note: Admin JWT is also accepted by admin routes, but machine-to-machine integration should use admin API key.
|
- `x-api-key: admin-<64hex>` (recommended for server-to-server calls)
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
|
||||||
## 1) Create + Redeem in One Step
|
Note: Admin JWT is also accepted by admin routes, but Admin API key is recommended for machine integrations.
|
||||||
|
|
||||||
|
### 1) One-step Create + Redeem
|
||||||
|
|
||||||
`POST /api/v1/admin/redeem-codes/create-and-redeem`
|
`POST /api/v1/admin/redeem-codes/create-and-redeem`
|
||||||
|
|
||||||
Purpose:
|
Purpose:
|
||||||
|
|
||||||
- Atomically create a deterministic redeem code and redeem it to a target user.
|
- Atomically create a deterministic redeem code and redeem it to the target user.
|
||||||
- Typical usage: called after payment callback succeeds.
|
- Typical usage: called right after payment callback success.
|
||||||
|
|
||||||
Required headers:
|
Required headers:
|
||||||
|
|
||||||
@@ -42,16 +204,16 @@ Request body:
|
|||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- `code`: external deterministic order-mapped code.
|
- `code`: deterministic code mapped from external order id.
|
||||||
- `type`: currently recommended `balance`.
|
- `type`: `balance` is the recommended type.
|
||||||
- `value`: must be `> 0`.
|
- `value`: must be greater than 0.
|
||||||
- `user_id`: target user id.
|
- `user_id`: target user id.
|
||||||
|
|
||||||
Idempotency semantics:
|
Idempotency behavior:
|
||||||
|
|
||||||
- Same `code`, same `used_by` user: return `200` (idempotent replay).
|
- Same `code` and same `used_by`: `200` (idempotent replay).
|
||||||
- Same `code`, different `used_by` user: return `409` conflict.
|
- Same `code` and different `used_by`: `409` (conflict).
|
||||||
- Missing `Idempotency-Key`: return `400` (`IDEMPOTENCY_KEY_REQUIRED`).
|
- Missing `Idempotency-Key`: `400` (`IDEMPOTENCY_KEY_REQUIRED`).
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -69,13 +231,13 @@ curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2) Query User (Optional Pre-check)
|
### 2) Query User (Optional Pre-check)
|
||||||
|
|
||||||
`GET /api/v1/admin/users/:id`
|
`GET /api/v1/admin/users/:id`
|
||||||
|
|
||||||
Purpose:
|
Purpose:
|
||||||
|
|
||||||
- Check whether target user exists before payment finalize/retry.
|
- Verify target user existence before final recharge/retry.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -84,13 +246,13 @@ curl -s "${BASE}/api/v1/admin/users/123" \
|
|||||||
-H "x-api-key: ${KEY}"
|
-H "x-api-key: ${KEY}"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3) Balance Adjustment (Existing Interface)
|
### 3) Balance Adjustment (Existing API)
|
||||||
|
|
||||||
`POST /api/v1/admin/users/:id/balance`
|
`POST /api/v1/admin/users/:id/balance`
|
||||||
|
|
||||||
Purpose:
|
Purpose:
|
||||||
|
|
||||||
- Existing reusable admin interface for manual correction.
|
- Reuse existing admin endpoint for manual reconciliation.
|
||||||
- Supports `set`, `add`, `subtract`.
|
- Supports `set`, `add`, `subtract`.
|
||||||
|
|
||||||
Request body example (`subtract`):
|
Request body example (`subtract`):
|
||||||
@@ -117,18 +279,35 @@ curl -X POST "${BASE}/api/v1/admin/users/123/balance" \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4) Error Handling Recommendations
|
### 4) Purchase URL Query Parameters (Iframe + New Tab)
|
||||||
|
|
||||||
- Persist upstream payment result independently from recharge result.
|
When Sub2API frontend opens `purchase_subscription_url`, it appends the same query parameters for both iframe and “open in new tab” to keep context consistent.
|
||||||
|
|
||||||
|
Appended parameters:
|
||||||
|
|
||||||
|
- `user_id`: current logged-in user id
|
||||||
|
- `token`: current logged-in JWT token
|
||||||
|
- `theme`: current theme (`light` / `dark`)
|
||||||
|
- `ui_mode`: UI mode (fixed `embedded`)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://pay.example.com/pay?user_id=123&token=<jwt>&theme=light&ui_mode=embedded
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5) Failure Handling Recommendations
|
||||||
|
|
||||||
|
- Store payment state and recharge state separately.
|
||||||
- Mark payment success immediately after callback verification.
|
- Mark payment success immediately after callback verification.
|
||||||
- If recharge fails after payment success, keep order retryable by admin operation.
|
- Keep orders retryable when payment succeeded but recharge failed.
|
||||||
- For retry, always reuse deterministic `code` + new `Idempotency-Key`.
|
- Reuse the same deterministic `code` and a new `Idempotency-Key` when retrying.
|
||||||
|
|
||||||
## 5) Suggested `doc_url` Setting
|
### 6) Suggested `doc_url` Value
|
||||||
|
|
||||||
Sub2API already supports `doc_url` in system settings.
|
Sub2API already supports `doc_url` in system settings.
|
||||||
|
|
||||||
Recommended values:
|
Recommended values:
|
||||||
|
|
||||||
- View URL: `https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md`
|
- View URL: `https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md`
|
||||||
- Direct download URL: `https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/ADMIN_PAYMENT_INTEGRATION_API.md`
|
- Download URL: `https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/ADMIN_PAYMENT_INTEGRATION_API.md`
|
||||||
|
|||||||
@@ -1,30 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<div class="purchase-page-layout">
|
<div class="purchase-page-layout">
|
||||||
<div class="flex items-start justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
{{ t('purchase.title') }}
|
|
||||||
</h2>
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-dark-400">
|
|
||||||
{{ t('purchase.description') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<a
|
|
||||||
v-if="isValidUrl"
|
|
||||||
:href="purchaseUrl"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="btn btn-secondary btn-sm"
|
|
||||||
>
|
|
||||||
<Icon name="externalLink" size="sm" class="mr-1.5" :stroke-width="2" />
|
|
||||||
{{ t('purchase.openInNewTab') }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card flex-1 min-h-0 overflow-hidden">
|
<div class="card flex-1 min-h-0 overflow-hidden">
|
||||||
<div v-if="loading" class="flex h-full items-center justify-center py-12">
|
<div v-if="loading" class="flex h-full items-center justify-center py-12">
|
||||||
<div
|
<div
|
||||||
@@ -70,30 +46,94 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<iframe v-else :src="purchaseUrl" class="h-full w-full border-0" allowfullscreen></iframe>
|
<div v-else class="purchase-embed-shell">
|
||||||
|
<a
|
||||||
|
:href="purchaseUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="btn btn-secondary btn-sm purchase-open-fab"
|
||||||
|
>
|
||||||
|
<Icon name="externalLink" size="sm" class="mr-1.5" :stroke-width="2" />
|
||||||
|
{{ t('purchase.openInNewTab') }}
|
||||||
|
</a>
|
||||||
|
<iframe
|
||||||
|
:src="purchaseUrl"
|
||||||
|
class="purchase-embed-frame"
|
||||||
|
allowfullscreen
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useAppStore } from '@/stores'
|
import { useAppStore } from '@/stores'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||||
import Icon from '@/components/icons/Icon.vue'
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const PURCHASE_USER_ID_QUERY_KEY = 'user_id'
|
||||||
|
const PURCHASE_AUTH_TOKEN_QUERY_KEY = 'token'
|
||||||
|
const PURCHASE_THEME_QUERY_KEY = 'theme'
|
||||||
|
const PURCHASE_UI_MODE_QUERY_KEY = 'ui_mode'
|
||||||
|
const PURCHASE_UI_MODE_EMBEDDED = 'embedded'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const purchaseTheme = ref<'light' | 'dark'>('light')
|
||||||
|
let themeObserver: MutationObserver | null = null
|
||||||
|
|
||||||
const purchaseEnabled = computed(() => {
|
const purchaseEnabled = computed(() => {
|
||||||
return appStore.cachedPublicSettings?.purchase_subscription_enabled ?? false
|
return appStore.cachedPublicSettings?.purchase_subscription_enabled ?? false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function detectTheme(): 'light' | 'dark' {
|
||||||
|
if (typeof document === 'undefined') return 'light'
|
||||||
|
return document.documentElement.classList.contains('dark') ? 'dark' : 'light'
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPurchaseUrl(
|
||||||
|
baseUrl: string,
|
||||||
|
userId?: number,
|
||||||
|
authToken?: string | null,
|
||||||
|
theme: 'light' | 'dark' = 'light',
|
||||||
|
): string {
|
||||||
|
if (!baseUrl) return baseUrl
|
||||||
|
try {
|
||||||
|
const url = new URL(baseUrl)
|
||||||
|
if (userId) {
|
||||||
|
url.searchParams.set(PURCHASE_USER_ID_QUERY_KEY, String(userId))
|
||||||
|
}
|
||||||
|
if (authToken) {
|
||||||
|
url.searchParams.set(PURCHASE_AUTH_TOKEN_QUERY_KEY, authToken)
|
||||||
|
}
|
||||||
|
url.searchParams.set(PURCHASE_THEME_QUERY_KEY, theme)
|
||||||
|
url.searchParams.set(PURCHASE_UI_MODE_QUERY_KEY, PURCHASE_UI_MODE_EMBEDDED)
|
||||||
|
return url.toString()
|
||||||
|
} catch {
|
||||||
|
const params: string[] = []
|
||||||
|
if (userId) {
|
||||||
|
params.push(`${PURCHASE_USER_ID_QUERY_KEY}=${encodeURIComponent(String(userId))}`)
|
||||||
|
}
|
||||||
|
if (authToken) {
|
||||||
|
params.push(`${PURCHASE_AUTH_TOKEN_QUERY_KEY}=${encodeURIComponent(authToken)}`)
|
||||||
|
}
|
||||||
|
params.push(`${PURCHASE_THEME_QUERY_KEY}=${encodeURIComponent(theme)}`)
|
||||||
|
params.push(`${PURCHASE_UI_MODE_QUERY_KEY}=${encodeURIComponent(PURCHASE_UI_MODE_EMBEDDED)}`)
|
||||||
|
const separator = baseUrl.includes('?') ? '&' : '?'
|
||||||
|
return `${baseUrl}${separator}${params.join('&')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const purchaseUrl = computed(() => {
|
const purchaseUrl = computed(() => {
|
||||||
return (appStore.cachedPublicSettings?.purchase_subscription_url || '').trim()
|
const baseUrl = (appStore.cachedPublicSettings?.purchase_subscription_url || '').trim()
|
||||||
|
return buildPurchaseUrl(baseUrl, authStore.user?.id, authStore.token, purchaseTheme.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const isValidUrl = computed(() => {
|
const isValidUrl = computed(() => {
|
||||||
@@ -102,6 +142,18 @@ const isValidUrl = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
purchaseTheme.value = detectTheme()
|
||||||
|
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
themeObserver = new MutationObserver(() => {
|
||||||
|
purchaseTheme.value = detectTheme()
|
||||||
|
})
|
||||||
|
themeObserver.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class'],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (appStore.publicSettingsLoaded) return
|
if (appStore.publicSettingsLoaded) return
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -110,12 +162,41 @@ onMounted(async () => {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (themeObserver) {
|
||||||
|
themeObserver.disconnect()
|
||||||
|
themeObserver = null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.purchase-page-layout {
|
.purchase-page-layout {
|
||||||
@apply flex flex-col gap-6;
|
@apply flex flex-col;
|
||||||
height: calc(100vh - 64px - 4rem); /* 减去 header + lg:p-8 的上下padding */
|
height: calc(100vh - 64px - 4rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase-embed-shell {
|
||||||
|
@apply relative;
|
||||||
|
@apply h-full w-full overflow-hidden rounded-2xl;
|
||||||
|
@apply bg-gradient-to-b from-gray-50 to-white dark:from-dark-900 dark:to-dark-950;
|
||||||
|
@apply p-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase-open-fab {
|
||||||
|
@apply absolute right-3 top-3 z-10;
|
||||||
|
@apply shadow-sm backdrop-blur supports-[backdrop-filter]:bg-white/80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purchase-embed-frame {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user