feat: integrate CREEM web payment for credits purchase

Replace abandoned iOS App Store route with CREEM Merchant of Record
payment integration for web-based credits purchase.

Backend changes:
- Add CreemClient for CREEM API communication
- Add CreemService for checkout creation and webhook handling
- Add creem_transactions table for payment tracking
- Fix webhook payload parsing (id, order.id, customer.id structure)
- Integrate with existing points ledger system

Frontend changes:
- Display dynamic prices from CREEM API
- Support decimal price formatting (e.g., $1.00)
- Add checkout flow with redirect to CREEM hosted page
This commit is contained in:
zl-q
2026-05-11 18:38:21 +08:00
parent 3ff33640f4
commit f07e307e82
25 changed files with 989 additions and 45 deletions
+19 -9
View File
@@ -1,4 +1,5 @@
import { logout, getAuth } from '../lib/auth';
import { useState } from 'react';
import { logout, getAuth, clearAuth, redirectToLogin } from '../lib/auth';
import { usePoints, useProfile } from '../lib/resources';
interface Props {
@@ -13,13 +14,19 @@ export default function SettingsPage({ locale, settings: s }: Props) {
const profile = profileState.data ?? null;
const points = pointsState.data ?? null;
const loading = profileState.loading || pointsState.loading;
const [logoutLoading, setLogoutLoading] = useState(false);
const handleLogout = () => {
if (confirm(s.logoutConfirm)) {
logout().finally(() => {
window.location.href = `/${locale}/login`;
});
}
const handleLogout = async () => {
if (logoutLoading) return;
if (!confirm(s.logoutConfirm)) return;
setLogoutLoading(true);
// Clear local auth immediately and redirect
clearAuth();
// Fire backend logout in background (don't wait)
logout().catch(() => {});
// Redirect to login
redirectToLogin();
};
const authEmail = getAuth()?.user?.email;
@@ -198,9 +205,12 @@ export default function SettingsPage({ locale, settings: s }: Props) {
{/* Logout Button */}
<button
onClick={handleLogout}
className="bg-white rounded-2xl px-5 py-3.5 border border-red-200 flex items-center justify-between hover:bg-red-50 transition-colors"
disabled={logoutLoading}
className={`bg-white rounded-2xl px-5 py-3.5 border border-red-200 flex items-center justify-between hover:bg-red-50 transition-colors ${logoutLoading ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<span className="text-red-500 text-sm font-medium">{s.logout}</span>
<span className="text-red-500 text-sm font-medium">
{logoutLoading ? (locale === 'en' ? 'Logging out...' : '退出中...') : s.logout}
</span>
<span className="material-symbols-rounded text-red-400 text-lg">logout</span>
</button>
</div>