diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 1601b86c2e0f..5f552b910ca1 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -2,16 +2,23 @@ name: Publish Docker image (Multi-arch) on: push: + branches: + - main tags: - '*' - '!nightly*' + - '!*-alpha*' workflow_dispatch: inputs: - tag: - description: 'Tag name to build (e.g., v0.10.8-alpha.3)' - required: true + ref: + description: 'Git ref to build (e.g., main or v0.10.8)' + required: false + default: 'main' type: string +env: + DOCKERHUB_IMAGE: calciumion/new-api + jobs: build_single_arch: name: Build & push (${{ matrix.arch }}) @@ -27,7 +34,9 @@ jobs: runner: ubuntu-24.04-arm runs-on: ${{ matrix.runner }} outputs: - tag: ${{ steps.version.outputs.tag }} + image_version: ${{ steps.version.outputs.image_version }} + publish_latest: ${{ steps.version.outputs.publish_latest }} + dockerhub_enabled: ${{ steps.registry_flags.outputs.dockerhub_enabled }} permissions: packages: write @@ -36,53 +45,128 @@ jobs: steps: - name: Check out - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: ${{ github.event_name == 'workflow_dispatch' && 0 || 1 }} - ref: ${{ github.event.inputs.tag || github.ref }} + ref: ${{ github.event.inputs.ref || github.ref }} - - name: Resolve tag & write VERSION + - name: Resolve image version & write VERSION id: version + shell: bash + env: + INPUT_REF: ${{ github.event.inputs.ref }} run: | - if [ -n "${{ github.event.inputs.tag }}" ]; then - TAG="${{ github.event.inputs.tag }}" - if ! git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then - echo "::error::Tag '$TAG' does not exist" - exit 1 + if [ -n "${INPUT_REF}" ]; then + NORMALIZED_REF="${INPUT_REF#refs/heads/}" + NORMALIZED_REF="${NORMALIZED_REF#refs/tags/}" + if git rev-parse "refs/tags/$NORMALIZED_REF" >/dev/null 2>&1; then + IMAGE_VERSION="$NORMALIZED_REF" + VERSION_KIND="tag" + PUBLISH_LATEST="false" + else + SHORT_SHA="$(git rev-parse --short HEAD)" + SAFE_REF="${NORMALIZED_REF//\//-}" + IMAGE_VERSION="${SAFE_REF}-${SHORT_SHA}" + VERSION_KIND="ref" + if [ "$NORMALIZED_REF" = "main" ]; then + PUBLISH_LATEST="true" + else + PUBLISH_LATEST="false" + fi fi + elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then + IMAGE_VERSION="${GITHUB_REF#refs/tags/}" + VERSION_KIND="tag" + PUBLISH_LATEST="false" else - TAG=${GITHUB_REF#refs/tags/} + SHORT_SHA="$(git rev-parse --short HEAD)" + BRANCH_NAME="${GITHUB_REF#refs/heads/}" + SAFE_BRANCH="${BRANCH_NAME//\//-}" + IMAGE_VERSION="${SAFE_BRANCH}-${SHORT_SHA}" + VERSION_KIND="branch" + if [ "$BRANCH_NAME" = "main" ]; then + PUBLISH_LATEST="true" + else + PUBLISH_LATEST="false" + fi fi - echo "TAG=${TAG}" >> $GITHUB_ENV - echo "tag=${TAG}" >> $GITHUB_OUTPUT - echo "${TAG}" > VERSION - echo "Building tag: ${TAG} for ${{ matrix.arch }}" + echo "IMAGE_VERSION=${IMAGE_VERSION}" >> $GITHUB_ENV + echo "VERSION_KIND=${VERSION_KIND}" >> $GITHUB_ENV + echo "PUBLISH_LATEST=${PUBLISH_LATEST}" >> $GITHUB_ENV + echo "image_version=${IMAGE_VERSION}" >> $GITHUB_OUTPUT + echo "publish_latest=${PUBLISH_LATEST}" >> $GITHUB_OUTPUT + echo "${IMAGE_VERSION}" > VERSION + echo "Building image version: ${IMAGE_VERSION} (${VERSION_KIND}) for ${{ matrix.arch }}; publish latest: ${PUBLISH_LATEST}" + + - name: Detect optional Docker Hub publish + id: registry_flags + run: echo "dockerhub_enabled=${{ secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }}" >> $GITHUB_OUTPUT - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Log in to Docker Hub - uses: docker/login-action@v3 + if: ${{ steps.registry_flags.outputs.dockerhub_enabled == 'true' }} + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Prepare registry image names + id: registry_names + shell: bash + run: | + OWNER_LC="$(printf '%s' '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" + GHCR_IMAGE="ghcr.io/${OWNER_LC}/new-api" + echo "GHCR_IMAGE=${GHCR_IMAGE}" >> "$GITHUB_ENV" + echo "ghcr_image=${GHCR_IMAGE}" >> "$GITHUB_OUTPUT" + - name: Extract metadata (labels) id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 with: - images: calciumion/new-api + images: ${{ steps.registry_names.outputs.ghcr_image }} + + - name: Prepare image tags + id: tags + shell: bash + env: + DOCKERHUB_ENABLED: ${{ steps.registry_flags.outputs.dockerhub_enabled }} + run: | + TAGS=() + TAGS+=("${GHCR_IMAGE}:${IMAGE_VERSION}-${{ matrix.arch }}") + if [ "${PUBLISH_LATEST}" = "true" ]; then + TAGS+=("${GHCR_IMAGE}:latest-${{ matrix.arch }}") + fi + + if [ "${DOCKERHUB_ENABLED}" = "true" ]; then + TAGS+=("${DOCKERHUB_IMAGE}:${IMAGE_VERSION}-${{ matrix.arch }}") + if [ "${PUBLISH_LATEST}" = "true" ]; then + TAGS+=("${DOCKERHUB_IMAGE}:latest-${{ matrix.arch }}") + fi + fi + + { + echo 'tags<> "$GITHUB_OUTPUT" - name: Build & push id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . platforms: ${{ matrix.platform }} push: true - tags: | - calciumion/new-api:${{ env.TAG }}-${{ matrix.arch }} - calciumion/new-api:latest-${{ matrix.arch }} + tags: ${{ steps.tags.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max @@ -90,16 +174,34 @@ jobs: sbom: true - name: Install cosign - uses: sigstore/cosign-installer@v3 + uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 - - name: Sign image with cosign - run: cosign sign --yes calciumion/new-api@${{ steps.build.outputs.digest }} + - name: Sign GHCR image with cosign + run: cosign sign --yes "${GHCR_IMAGE}@${{ steps.build.outputs.digest }}" + + - name: Sign Docker Hub image with cosign + if: ${{ steps.registry_flags.outputs.dockerhub_enabled == 'true' }} + run: cosign sign --yes "${DOCKERHUB_IMAGE}@${{ steps.build.outputs.digest }}" - name: Image summary + shell: bash + env: + DOCKERHUB_ENABLED: ${{ steps.registry_flags.outputs.dockerhub_enabled }} run: | - echo "### Docker Image Digest (${{ matrix.arch }})" >> $GITHUB_STEP_SUMMARY + echo "### Image Digest (${{ matrix.arch }})" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - echo "calciumion/new-api:${TAG}-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY + echo "${GHCR_IMAGE}:${IMAGE_VERSION}-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY + if [ "${PUBLISH_LATEST}" = "true" ]; then + echo "${GHCR_IMAGE}:latest-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY + fi + if [ "${DOCKERHUB_ENABLED}" = "true" ]; then + echo "${DOCKERHUB_IMAGE}:${IMAGE_VERSION}-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY + if [ "${PUBLISH_LATEST}" = "true" ]; then + echo "${DOCKERHUB_IMAGE}:latest-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY + fi + else + echo "Docker Hub publish skipped: DOCKERHUB_USERNAME / DOCKERHUB_TOKEN not configured." >> $GITHUB_STEP_SUMMARY + fi echo "${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY @@ -107,35 +209,84 @@ jobs: name: Create multi-arch manifests needs: [build_single_arch] runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' + permissions: + packages: write + contents: read steps: - - name: Set version - run: echo "TAG=${{ needs.build_single_arch.outputs.tag }}" >> $GITHUB_ENV + - name: Set image version + shell: bash + run: | + OWNER_LC="$(printf '%s' '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" + echo "GHCR_IMAGE=ghcr.io/${OWNER_LC}/new-api" >> $GITHUB_ENV + echo "IMAGE_VERSION=${{ needs.build_single_arch.outputs.image_version }}" >> $GITHUB_ENV + echo "PUBLISH_LATEST=${{ needs.build_single_arch.outputs.publish_latest }}" >> $GITHUB_ENV + echo "DOCKERHUB_ENABLED=${{ needs.build_single_arch.outputs.dockerhub_enabled }}" >> $GITHUB_ENV + + - name: Log in to GitHub Container Registry + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Log in to Docker Hub - uses: docker/login-action@v3 + if: ${{ needs.build_single_arch.outputs.dockerhub_enabled == 'true' }} + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Create & push manifest (version) + - name: Create & push GHCR manifest (version) run: | docker buildx imagetools create \ - -t calciumion/new-api:${TAG} \ - calciumion/new-api:${TAG}-amd64 \ - calciumion/new-api:${TAG}-arm64 + -t ${GHCR_IMAGE}:${IMAGE_VERSION} \ + ${GHCR_IMAGE}:${IMAGE_VERSION}-amd64 \ + ${GHCR_IMAGE}:${IMAGE_VERSION}-arm64 - - name: Create & push manifest (latest) + - name: Create & push GHCR manifest (latest) + if: ${{ needs.build_single_arch.outputs.publish_latest == 'true' }} run: | docker buildx imagetools create \ - -t calciumion/new-api:latest \ - calciumion/new-api:latest-amd64 \ - calciumion/new-api:latest-arm64 + -t ${GHCR_IMAGE}:latest \ + ${GHCR_IMAGE}:latest-amd64 \ + ${GHCR_IMAGE}:latest-arm64 + + - name: Create & push Docker Hub manifest (version) + if: ${{ needs.build_single_arch.outputs.dockerhub_enabled == 'true' }} + run: | + docker buildx imagetools create \ + -t ${DOCKERHUB_IMAGE}:${IMAGE_VERSION} \ + ${DOCKERHUB_IMAGE}:${IMAGE_VERSION}-amd64 \ + ${DOCKERHUB_IMAGE}:${IMAGE_VERSION}-arm64 + + - name: Create & push Docker Hub manifest (latest) + if: ${{ needs.build_single_arch.outputs.dockerhub_enabled == 'true' && needs.build_single_arch.outputs.publish_latest == 'true' }} + run: | + docker buildx imagetools create \ + -t ${DOCKERHUB_IMAGE}:latest \ + ${DOCKERHUB_IMAGE}:latest-amd64 \ + ${DOCKERHUB_IMAGE}:latest-arm64 - name: Manifest summary + shell: bash run: | echo "### Multi-arch Manifest" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - docker buildx imagetools inspect calciumion/new-api:${TAG} >> $GITHUB_STEP_SUMMARY + docker buildx imagetools inspect ${GHCR_IMAGE}:${IMAGE_VERSION} >> $GITHUB_STEP_SUMMARY + if [ "${PUBLISH_LATEST}" = "true" ]; then + echo "---" >> $GITHUB_STEP_SUMMARY + docker buildx imagetools inspect ${GHCR_IMAGE}:latest >> $GITHUB_STEP_SUMMARY + fi + if [ "${DOCKERHUB_ENABLED}" = "true" ]; then + echo "---" >> $GITHUB_STEP_SUMMARY + docker buildx imagetools inspect ${DOCKERHUB_IMAGE}:${IMAGE_VERSION} >> $GITHUB_STEP_SUMMARY + if [ "${PUBLISH_LATEST}" = "true" ]; then + echo "---" >> $GITHUB_STEP_SUMMARY + docker buildx imagetools inspect ${DOCKERHUB_IMAGE}:latest >> $GITHUB_STEP_SUMMARY + fi + else + echo "---" >> $GITHUB_STEP_SUMMARY + echo "Docker Hub manifest skipped: DOCKERHUB_USERNAME / DOCKERHUB_TOKEN not configured." >> $GITHUB_STEP_SUMMARY + fi echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/controller/topup.go b/controller/topup.go index 69e1b5e304c4..efd880bea221 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -118,6 +118,7 @@ func GetTopUpInfo(c *gin.Context) { "waffo_pancake_min_topup": setting.WaffoPancakeMinTopUp, "amount_options": operation_setting.GetPaymentSetting().AmountOptions, "discount": operation_setting.GetPaymentSetting().AmountDiscount, + "default_topup_amount": operation_setting.GetPaymentSetting().DefaultTopUpAmount, "topup_link": common.TopUpLink, } common.ApiSuccess(c, data) diff --git a/setting/operation_setting/payment_setting.go b/setting/operation_setting/payment_setting.go index b08d466b510f..83b06aced7c8 100644 --- a/setting/operation_setting/payment_setting.go +++ b/setting/operation_setting/payment_setting.go @@ -3,26 +3,25 @@ package operation_setting import "github.com/QuantumNous/new-api/setting/config" type PaymentSetting struct { - AmountOptions []int `json:"amount_options"` - AmountDiscount map[int]float64 `json:"amount_discount"` // 充值金额对应的折扣,例如 100 元 0.9 表示 100 元充值享受 9 折优惠 - - ComplianceConfirmed bool `json:"compliance_confirmed"` - ComplianceTermsVersion string `json:"compliance_terms_version"` - ComplianceConfirmedAt int64 `json:"compliance_confirmed_at"` - ComplianceConfirmedBy int `json:"compliance_confirmed_by"` - ComplianceConfirmedIP string `json:"compliance_confirmed_ip"` + AmountOptions []int `json:"amount_options"` + AmountDiscount map[int]float64 `json:"amount_discount"` + DefaultTopUpAmount int `json:"default_topup_amount"` + ComplianceConfirmed bool `json:"compliance_confirmed"` + ComplianceTermsVersion string `json:"compliance_terms_version"` + ComplianceConfirmedAt int64 `json:"compliance_confirmed_at"` + ComplianceConfirmedBy int `json:"compliance_confirmed_by"` + ComplianceConfirmedIP string `json:"compliance_confirmed_ip"` } const CurrentComplianceTermsVersion = "v1" -// 默认配置 var paymentSetting = PaymentSetting{ - AmountOptions: []int{10, 20, 50, 100, 200, 500}, - AmountDiscount: map[int]float64{}, + AmountOptions: []int{10, 20, 50, 100, 200, 500}, + AmountDiscount: map[int]float64{}, + DefaultTopUpAmount: 100, } func init() { - // 注册到全局配置管理器 config.GlobalConfig.Register("payment_setting", &paymentSetting) } diff --git a/web/classic/src/components/settings/PaymentSetting.jsx b/web/classic/src/components/settings/PaymentSetting.jsx index 880001123d5a..08c8c8f788c6 100644 --- a/web/classic/src/components/settings/PaymentSetting.jsx +++ b/web/classic/src/components/settings/PaymentSetting.jsx @@ -44,6 +44,7 @@ const PaymentSetting = () => { PayMethods: '', AmountOptions: '', AmountDiscount: '', + DefaultTopUpAmount: 100, StripeApiSecret: '', StripeWebhookSecret: '', @@ -146,6 +147,9 @@ const PaymentSetting = () => { newInputs['AmountDiscount'] = item.value; } break; + case 'payment_setting.default_topup_amount': + newInputs['DefaultTopUpAmount'] = parseFloat(item.value) || 100; + break; case 'payment_setting.compliance_confirmed': newInputs[item.key] = toBoolean(item.value); break; @@ -158,6 +162,7 @@ const PaymentSetting = () => { break; case 'Price': case 'MinTopUp': + case 'DefaultTopUpAmount': case 'StripeUnitPrice': case 'StripeMinTopUp': newInputs[item.key] = parseFloat(item.value); diff --git a/web/classic/src/components/topup/RechargeCard.jsx b/web/classic/src/components/topup/RechargeCard.jsx index 4fe2035a1a35..674dbe828aa3 100644 --- a/web/classic/src/components/topup/RechargeCard.jsx +++ b/web/classic/src/components/topup/RechargeCard.jsx @@ -272,9 +272,9 @@ const RechargeCard = ({ }} onBlur={(e) => { const value = parseInt(e.target.value); - if (!value || value < 1) { - setTopUpCount(1); - getAmount(1); + if (!value || value < minTopUp) { + setTopUpCount(minTopUp); + getAmount(minTopUp); } }} formatter={(value) => (value ? `${value}` : '')} diff --git a/web/classic/src/components/topup/index.jsx b/web/classic/src/components/topup/index.jsx index 4d89253ba350..43ad27efb094 100644 --- a/web/classic/src/components/topup/index.jsx +++ b/web/classic/src/components/topup/index.jsx @@ -57,6 +57,16 @@ function isSafeHttpCheckoutUrl(value) { } } +function getDefaultTopUpCount(minAmount, defaultAmount = 100) { + const normalizedMinAmount = Number(minAmount) || 1; + const numericDefaultAmount = Number(defaultAmount); + const normalizedDefaultAmount = + Number.isFinite(numericDefaultAmount) && numericDefaultAmount > 0 + ? numericDefaultAmount + : 100; + return Math.max(normalizedDefaultAmount, normalizedMinAmount); +} + const TopUp = () => { const { t } = useTranslation(); const [searchParams, setSearchParams] = useSearchParams(); @@ -67,7 +77,7 @@ const TopUp = () => { const [amount, setAmount] = useState(0.0); const [minTopUp, setMinTopUp] = useState(statusState?.status?.min_topup || 1); const [topUpCount, setTopUpCount] = useState( - statusState?.status?.min_topup || 1, + getDefaultTopUpCount(statusState?.status?.min_topup || 1), ); const [topUpLink, setTopUpLink] = useState(''); const [enableOnlineTopUp, setEnableOnlineTopUp] = useState( @@ -127,6 +137,7 @@ const TopUp = () => { const [topupInfo, setTopupInfo] = useState({ amount_options: [], discount: {}, + default_topup_amount: 100, enable_redemption: true, payment_compliance_confirmed: true, }); @@ -601,6 +612,7 @@ const TopUp = () => { setTopupInfo({ amount_options: data.amount_options || [], discount: data.discount || {}, + default_topup_amount: data.default_topup_amount || 100, }); // 处理支付方式 @@ -678,7 +690,11 @@ const TopUp = () => { setEnableWaffoPancakeTopUp(enableWaffoPancakeTopUp); setWaffoPancakeMinTopUp(data.waffo_pancake_min_topup || 1); setMinTopUp(minTopUpValue); - setTopUpCount(minTopUpValue); + const defaultTopUpCount = getDefaultTopUpCount( + minTopUpValue, + data.default_topup_amount, + ); + setTopUpCount(defaultTopUpCount); setTopUpLink(data.topup_link || ''); setTopupInfo((prev) => ({ ...prev, @@ -703,7 +719,7 @@ const TopUp = () => { } // 初始化显示实付金额 - getAmount(minTopUpValue); + getAmount(defaultTopUpCount); } catch (e) { setPayMethods([]); } diff --git a/web/classic/src/pages/Setting/Payment/SettingsGeneralPayment.jsx b/web/classic/src/pages/Setting/Payment/SettingsGeneralPayment.jsx index 995b1c8cee52..f06c58adab66 100644 --- a/web/classic/src/pages/Setting/Payment/SettingsGeneralPayment.jsx +++ b/web/classic/src/pages/Setting/Payment/SettingsGeneralPayment.jsx @@ -39,6 +39,7 @@ export default function SettingsGeneralPayment(props) { PayMethods: '', AmountOptions: '', AmountDiscount: '', + DefaultTopUpAmount: 100, }); const [originInputs, setOriginInputs] = useState({}); const formApiRef = useRef(null); @@ -52,6 +53,10 @@ export default function SettingsGeneralPayment(props) { PayMethods: props.options.PayMethods || '', AmountOptions: props.options.AmountOptions || '', AmountDiscount: props.options.AmountDiscount || '', + DefaultTopUpAmount: + props.options.DefaultTopUpAmount !== undefined + ? Number(props.options.DefaultTopUpAmount) || 100 + : 100, }; setInputs(currentInputs); setOriginInputs({ ...currentInputs }); @@ -131,6 +136,12 @@ export default function SettingsGeneralPayment(props) { value: inputs.AmountDiscount, }); } + if (originInputs.DefaultTopUpAmount !== inputs.DefaultTopUpAmount) { + options.push({ + key: 'payment_setting.default_topup_amount', + value: String(inputs.DefaultTopUpAmount), + }); + } const results = await Promise.all( options.map((option) => @@ -209,6 +220,20 @@ export default function SettingsGeneralPayment(props) { autosize /> + + + + + - - - + { const trimmed = value.trim() if (!trimmed) return true @@ -405,6 +406,7 @@ export function PaymentSettingsSection({ EpayKey: values.EpayKey.trim(), Price: values.Price, MinTopUp: values.MinTopUp, + DefaultTopUpAmount: values.DefaultTopUpAmount, CustomCallbackAddress: removeTrailingSlash(values.CustomCallbackAddress), PayMethods: values.PayMethods.trim(), AmountOptions: values.AmountOptions.trim(), @@ -447,6 +449,7 @@ export function PaymentSettingsSection({ EpayKey: initialRef.current.EpayKey.trim(), Price: initialRef.current.Price, MinTopUp: initialRef.current.MinTopUp, + DefaultTopUpAmount: initialRef.current.DefaultTopUpAmount, CustomCallbackAddress: removeTrailingSlash( initialRef.current.CustomCallbackAddress ), @@ -510,6 +513,13 @@ export function PaymentSettingsSection({ updates.push({ key: 'MinTopUp', value: sanitized.MinTopUp }) } + if (sanitized.DefaultTopUpAmount !== initial.DefaultTopUpAmount) { + updates.push({ + key: 'payment_setting.default_topup_amount', + value: sanitized.DefaultTopUpAmount, + }) + } + if (sanitized.CustomCallbackAddress !== initial.CustomCallbackAddress) { updates.push({ key: 'CustomCallbackAddress', @@ -865,7 +875,7 @@ export function PaymentSettingsSection({

-
+
)} /> + + ( + + {t('Default top-up amount')} + + + + + {t( + 'Initial amount shown in the top-up input before the user edits it.' + )} + + + + )} + />
{ if (topupInfo && topupAmount === 0) { - const minTopup = getMinTopupAmount(topupInfo) - setTopupAmount(minTopup) + const defaultTopupAmount = getDefaultCustomTopupAmount(topupInfo) + setTopupAmount(defaultTopupAmount) // Calculate initial payment amount with default payment type const defaultPaymentType = getDefaultPaymentType(topupInfo) - calculatePaymentAmount(minTopup, defaultPaymentType) + calculatePaymentAmount(defaultTopupAmount, defaultPaymentType) } }, [topupInfo, topupAmount, calculatePaymentAmount]) diff --git a/web/default/src/features/wallet/lib/payment.ts b/web/default/src/features/wallet/lib/payment.ts index 84d1a20e7ce7..c834ee3837cd 100644 --- a/web/default/src/features/wallet/lib/payment.ts +++ b/web/default/src/features/wallet/lib/payment.ts @@ -21,6 +21,7 @@ import { DEFAULT_PRESET_MULTIPLIERS, DEFAULT_PAYMENT_TYPE, DEFAULT_MIN_TOPUP, + DEFAULT_CUSTOM_TOPUP_AMOUNT, } from '../constants' import type { PresetAmount, TopupInfo } from '../types' @@ -141,6 +142,24 @@ export function getMinTopupAmount(topupInfo: TopupInfo | null): number { return DEFAULT_MIN_TOPUP } +/** + * Get the default custom topup amount shown in the input field. + * Prefer 100 by default, but never go below the active minimum topup. + */ +export function getDefaultCustomTopupAmount( + topupInfo: TopupInfo | null +): number { + const minTopup = getMinTopupAmount(topupInfo) + const normalizedMinTopup = + Number.isFinite(minTopup) && minTopup > 0 ? minTopup : DEFAULT_MIN_TOPUP + const configuredDefault = Number(topupInfo?.default_topup_amount) + const normalizedConfiguredDefault = + Number.isFinite(configuredDefault) && configuredDefault > 0 + ? configuredDefault + : DEFAULT_CUSTOM_TOPUP_AMOUNT + return Math.max(normalizedConfiguredDefault, normalizedMinTopup) +} + /** * Generate preset amounts based on minimum topup */ diff --git a/web/default/src/features/wallet/types.ts b/web/default/src/features/wallet/types.ts index f4ab21937323..53b50d165c0e 100644 --- a/web/default/src/features/wallet/types.ts +++ b/web/default/src/features/wallet/types.ts @@ -134,6 +134,8 @@ export interface TopupInfo { amount_options: number[] /** Discount rates by amount */ discount: Record + /** Default amount shown before the user edits the top-up input */ + default_topup_amount?: number /** Optional topup link for purchasing codes */ topup_link?: string /** Whether Creem topup is enabled */ diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 6b4c5970302c..711c7735c231 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -1106,6 +1106,7 @@ "Default system prompt for this channel": "Default system prompt for this channel", "Default time granularity": "Default time granularity", "Default to auto groups": "Default to auto groups", + "Default top-up amount": "Default top-up amount", "Default TTL (seconds)": "Default TTL (seconds)", "Defaults to the wallet page when empty": "Defaults to the wallet page when empty", "Define API endpoints for this model (JSON format)": "Define API endpoints for this model (JSON format)", @@ -2022,6 +2023,7 @@ "Includes request rules": "Includes request rules", "Including failed requests, 0 = unlimited": "Including failed requests, 0 = unlimited", "Index": "Index", + "Initial amount shown in the top-up input before the user edits it.": "Initial amount shown in the top-up input before the user edits it.", "Initial quota given to new users": "Initial quota given to new users", "Initialization failed, please try again.": "Initialization failed, please try again.", "Initialize": "Initialize", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index c9046cd5e06d..c9512f330959 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -1106,6 +1106,7 @@ "Default system prompt for this channel": "Invite système par défaut pour ce canal", "Default time granularity": "Granularité temporelle par défaut", "Default to auto groups": "Par défaut aux groupes automatiques", + "Default top-up amount": "Montant de recharge par défaut", "Default TTL (seconds)": "TTL par défaut (secondes)", "Defaults to the wallet page when empty": "Si vide, la page portefeuille est utilisée par défaut", "Define API endpoints for this model (JSON format)": "Définir les points de terminaison API pour ce modèle (format JSON)", @@ -2022,6 +2023,7 @@ "Includes request rules": "Inclut des règles de requête", "Including failed requests, 0 = unlimited": "Y compris les requêtes échouées, 0 = illimité", "Index": "Index", + "Initial amount shown in the top-up input before the user edits it.": "Montant initial affiché dans le champ de recharge avant modification par l'utilisateur.", "Initial quota given to new users": "Quota initial donné aux nouveaux utilisateurs", "Initialization failed, please try again.": "L'initialisation a échoué, veuillez réessayer.", "Initialize": "Initialiser", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index 51b77317d3ee..208f185cb4c7 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -1106,6 +1106,7 @@ "Default system prompt for this channel": "このチャンネルのデフォルトのシステムプロンプト", "Default time granularity": "デフォルトの時間粒度", "Default to auto groups": "デフォルトで自動グループ化", + "Default top-up amount": "デフォルトのチャージ金額", "Default TTL (seconds)": "デフォルト TTL(秒)", "Defaults to the wallet page when empty": "空欄の場合はウォレットページを既定にします", "Define API endpoints for this model (JSON format)": "このモデルのAPIエンドポイントを定義します (JSON形式)", @@ -2022,6 +2023,7 @@ "Includes request rules": "リクエストルールを含む", "Including failed requests, 0 = unlimited": "失敗したリクエストを含む、0 = 無制限", "Index": "インデックス", + "Initial amount shown in the top-up input before the user edits it.": "ユーザーが編集する前に、チャージ入力欄に最初に表示される金額です。", "Initial quota given to new users": "新規ユーザーに付与される初期クォータ", "Initialization failed, please try again.": "初期化に失敗しました。もう一度お試しください。", "Initialize": "初期化", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index 8362dc5e916a..aa65c53d7733 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -1106,6 +1106,7 @@ "Default system prompt for this channel": "Системный промпт по умолчанию для этого канала", "Default time granularity": "Гранулярность времени по умолчанию", "Default to auto groups": "По умолчанию использовать автогруппы", + "Default top-up amount": "Сумма пополнения по умолчанию", "Default TTL (seconds)": "TTL по умолчанию (секунды)", "Defaults to the wallet page when empty": "Если пусто, по умолчанию открывается страница кошелька", "Define API endpoints for this model (JSON format)": "Определить конечные точки API для этой модели (формат JSON)", @@ -2022,6 +2023,7 @@ "Includes request rules": "Включает правила запросов", "Including failed requests, 0 = unlimited": "Включая неудачные запросы, 0 = без ограничений", "Index": "Индекс", + "Initial amount shown in the top-up input before the user edits it.": "Начальная сумма, которая отображается в поле пополнения до редактирования пользователем.", "Initial quota given to new users": "Начальная квота, предоставляемая новым пользователям", "Initialization failed, please try again.": "Инициализация не удалась, попробуйте ещё раз.", "Initialize": "Инициализировать", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index 4fa8b74f33a1..b90397a1e0ff 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -1106,6 +1106,7 @@ "Default system prompt for this channel": "Lời nhắc hệ thống mặc định cho kênh này", "Default time granularity": "Độ chi tiết thời gian mặc định", "Default to auto groups": "Mặc định là nhóm tự động", + "Default top-up amount": "Số tiền nạp mặc định", "Default TTL (seconds)": "TTL mặc định (giây)", "Defaults to the wallet page when empty": "Để trống sẽ dùng trang ví mặc định", "Define API endpoints for this model (JSON format)": "Định nghĩa các điểm cuối API cho mô hình này (định dạng JSON)", @@ -2022,6 +2023,7 @@ "Includes request rules": "Bao gồm quy tắc yêu cầu", "Including failed requests, 0 = unlimited": "Bao gồm các yêu cầu thất bại, 0 = không giới hạn", "Index": "Chỉ mục", + "Initial amount shown in the top-up input before the user edits it.": "Số tiền ban đầu hiển thị trong ô nạp tiền trước khi người dùng chỉnh sửa.", "Initial quota given to new users": "Hạn mức ban đầu cấp cho người dùng mới", "Initialization failed, please try again.": "Khởi tạo thất bại, vui lòng thử lại.", "Initialize": "Khởi tạo", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index e198228d45c7..c8a27549d2c1 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -1106,6 +1106,7 @@ "Default system prompt for this channel": "此渠道的默认系统提示", "Default time granularity": "默认时间粒度", "Default to auto groups": "默认使用自动分组", + "Default top-up amount": "默认充值金额", "Default TTL (seconds)": "默认 TTL(秒)", "Defaults to the wallet page when empty": "为空时默认使用钱包页面", "Define API endpoints for this model (JSON format)": "为此模型定义 API 端点(JSON 格式)", @@ -2022,6 +2023,7 @@ "Includes request rules": "包含请求规则", "Including failed requests, 0 = unlimited": "包括失败的请求,0 = 无限制", "Index": "索引", + "Initial amount shown in the top-up input before the user edits it.": "用户进入充值页时输入框默认显示的金额", "Initial quota given to new users": "授予新用户的初始配额", "Initialization failed, please try again.": "初始化失败,请重试。", "Initialize": "初始化",