api: use node api endpoint for iap verifying (#1683)
This commit is contained in:
@@ -33,7 +33,7 @@
|
||||
"release-ios-appstore": "dotenv -e .env.ios-appstore.local -- bash scripts/release-ios-appstore.sh",
|
||||
"release-google-play": "dotenv -e .env.google-play.local -- bash scripts/release-google-play.sh",
|
||||
"config-wrangler": "sed -i \"s/\\${TRANSLATIONS_KV_ID}/$TRANSLATIONS_KV_ID/g\" wrangler.toml",
|
||||
"preview": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||
"preview": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare preview --ip 0.0.0.0",
|
||||
"deploy": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||
"upload": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
|
||||
@@ -46,7 +46,7 @@
|
||||
"@aws-sdk/s3-request-presigner": "^3.735.0",
|
||||
"@ducanh2912/next-pwa": "^10.2.9",
|
||||
"@fabianlars/tauri-plugin-oauth": "2",
|
||||
"@opennextjs/cloudflare": "^1.3.1",
|
||||
"@opennextjs/cloudflare": "^1.6.1",
|
||||
"@stripe/react-stripe-js": "^3.7.0",
|
||||
"@stripe/stripe-js": "^7.4.0",
|
||||
"@supabase/auth-ui-react": "^0.4.7",
|
||||
@@ -106,7 +106,7 @@
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/cssbeautify": "^0.3.5",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-color": "^3.0.13",
|
||||
"@types/react-dom": "18.3.1",
|
||||
@@ -132,6 +132,6 @@
|
||||
"typescript": "^5.7.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4",
|
||||
"wrangler": "^4.21.0"
|
||||
"wrangler": "^4.26.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,5 +474,6 @@
|
||||
"Failed to restore purchases.": "فشل في استعادة المشتريات.",
|
||||
"Failed to manage subscription.": "فشل في إدارة الاشتراك.",
|
||||
"Failed to load subscription plans.": "فشل في تحميل خطط الاشتراك.",
|
||||
"year": "سنة"
|
||||
"year": "سنة",
|
||||
"Failed to create checkout session": "فشل في إنشاء جلسة الدفع"
|
||||
}
|
||||
|
||||
@@ -458,5 +458,6 @@
|
||||
"Failed to restore purchases.": "Wiederherstellung der Käufe fehlgeschlagen.",
|
||||
"Failed to manage subscription.": "Verwaltung des Abonnements fehlgeschlagen.",
|
||||
"Failed to load subscription plans.": "Laden der Abonnementpläne fehlgeschlagen.",
|
||||
"year": "Jahr"
|
||||
"year": "Jahr",
|
||||
"Failed to create checkout session": "Fehler beim Erstellen der Checkout-Sitzung"
|
||||
}
|
||||
|
||||
@@ -458,5 +458,6 @@
|
||||
"Failed to restore purchases.": "Αποτυχία επαναφοράς αγορών.",
|
||||
"Failed to manage subscription.": "Αποτυχία διαχείρισης συνδρομής.",
|
||||
"Failed to load subscription plans.": "Αποτυχία φόρτωσης σχεδίων συνδρομής.",
|
||||
"year": "έτος"
|
||||
"year": "έτος",
|
||||
"Failed to create checkout session": "Αποτυχία δημιουργίας συνεδρίας πληρωμής"
|
||||
}
|
||||
|
||||
@@ -462,5 +462,6 @@
|
||||
"Failed to restore purchases.": "Error al restaurar compras.",
|
||||
"Failed to manage subscription.": "Error al gestionar la suscripción.",
|
||||
"Failed to load subscription plans.": "Error al cargar los planes de suscripción.",
|
||||
"year": "año"
|
||||
"year": "año",
|
||||
"Failed to create checkout session": "Error al crear la sesión de pago"
|
||||
}
|
||||
|
||||
@@ -462,5 +462,6 @@
|
||||
"Failed to restore purchases.": "Échec de la restauration des achats.",
|
||||
"Failed to manage subscription.": "Échec de la gestion de l'abonnement.",
|
||||
"Failed to load subscription plans.": "Échec du chargement des plans d'abonnement.",
|
||||
"year": "année"
|
||||
"year": "année",
|
||||
"Failed to create checkout session": "Échec de la création de la session de paiement"
|
||||
}
|
||||
|
||||
@@ -458,5 +458,6 @@
|
||||
"Failed to restore purchases.": "खरीदारी पुनर्स्थापित करने में विफल।",
|
||||
"Failed to manage subscription.": "सदस्यता प्रबंधित करने में विफल।",
|
||||
"Failed to load subscription plans.": "सदस्यता योजनाएँ लोड करने में विफल।",
|
||||
"year": "वर्ष"
|
||||
"year": "वर्ष",
|
||||
"Failed to create checkout session": "चेकआउट सत्र बनाने में विफल"
|
||||
}
|
||||
|
||||
@@ -454,5 +454,6 @@
|
||||
"Failed to restore purchases.": "Gagal memulihkan pembelian.",
|
||||
"Failed to manage subscription.": "Gagal mengelola langganan.",
|
||||
"Failed to load subscription plans.": "Gagal memuat rencana langganan.",
|
||||
"year": "tahun"
|
||||
"year": "tahun",
|
||||
"Failed to create checkout session": "Failed to create checkout session"
|
||||
}
|
||||
|
||||
@@ -462,5 +462,6 @@
|
||||
"Failed to restore purchases.": "Impossibile ripristinare gli acquisti.",
|
||||
"Failed to manage subscription.": "Impossibile gestire l'abbonamento.",
|
||||
"Failed to load subscription plans.": "Impossibile caricare i piani di abbonamento.",
|
||||
"year": "anno"
|
||||
"year": "anno",
|
||||
"Failed to create checkout session": "Impossibile creare la sessione di checkout"
|
||||
}
|
||||
|
||||
@@ -454,5 +454,6 @@
|
||||
"Failed to restore purchases.": "購入の復元に失敗しました。",
|
||||
"Failed to manage subscription.": "サブスクリプションの管理に失敗しました。",
|
||||
"Failed to load subscription plans.": "サブスクリプションプランの読み込みに失敗しました。",
|
||||
"year": "年"
|
||||
"year": "年",
|
||||
"Failed to create checkout session": "チェックアウトセッションの作成に失敗しました"
|
||||
}
|
||||
|
||||
@@ -454,5 +454,6 @@
|
||||
"Failed to restore purchases.": "구매 복원에 실패했습니다.",
|
||||
"Failed to manage subscription.": "구독 관리에 실패했습니다.",
|
||||
"Failed to load subscription plans.": "구독 플랜 로드에 실패했습니다.",
|
||||
"year": "년"
|
||||
"year": "년",
|
||||
"Failed to create checkout session": "체크아웃 세션 생성 실패"
|
||||
}
|
||||
|
||||
@@ -458,5 +458,6 @@
|
||||
"Failed to restore purchases.": "Herstellen van aankopen is mislukt.",
|
||||
"Failed to manage subscription.": "Beheren van abonnement is mislukt.",
|
||||
"Failed to load subscription plans.": "Laden van abonnement plannen is mislukt.",
|
||||
"year": "jaar"
|
||||
"year": "jaar",
|
||||
"Failed to create checkout session": "Kon de afreken sessie niet aanmaken"
|
||||
}
|
||||
|
||||
@@ -466,5 +466,6 @@
|
||||
"Failed to restore purchases.": "Nie udało się przywrócić zakupów.",
|
||||
"Failed to manage subscription.": "Nie udało się zarządzać subskrypcją.",
|
||||
"Failed to load subscription plans.": "Nie udało się załadować planów subskrypcyjnych.",
|
||||
"year": "rok"
|
||||
"year": "rok",
|
||||
"Failed to create checkout session": "Nie udało się utworzyć sesji zakupu"
|
||||
}
|
||||
|
||||
@@ -462,5 +462,6 @@
|
||||
"Failed to restore purchases.": "Falha ao restaurar compras.",
|
||||
"Failed to manage subscription.": "Falha ao gerenciar assinatura.",
|
||||
"Failed to load subscription plans.": "Falha ao carregar planos de assinatura.",
|
||||
"year": "ano"
|
||||
"year": "ano",
|
||||
"Failed to create checkout session": "Falha ao criar sessão de checkout"
|
||||
}
|
||||
|
||||
@@ -466,5 +466,6 @@
|
||||
"Failed to restore purchases.": "Не удалось восстановить покупки.",
|
||||
"Failed to manage subscription.": "Не удалось управлять подпиской.",
|
||||
"Failed to load subscription plans.": "Не удалось загрузить планы подписки.",
|
||||
"year": "год"
|
||||
"year": "год",
|
||||
"Failed to create checkout session": "Не удалось создать сессию оформления заказа"
|
||||
}
|
||||
|
||||
@@ -454,5 +454,6 @@
|
||||
"Failed to restore purchases.": "ไม่สามารถกู้คืนการซื้อได้",
|
||||
"Failed to manage subscription.": "ไม่สามารถจัดการการสมัครสมาชิกได้",
|
||||
"Failed to load subscription plans.": "ไม่สามารถโหลดแผนการสมัครสมาชิกได้",
|
||||
"year": "ปี"
|
||||
"year": "ปี",
|
||||
"Failed to create checkout session": "ไม่สามารถสร้างเซสชันการชำระเงินได้"
|
||||
}
|
||||
|
||||
@@ -458,5 +458,6 @@
|
||||
"Failed to restore purchases.": "Satın alımları geri yükleme başarısız oldu.",
|
||||
"Failed to manage subscription.": "Aboneliği yönetme başarısız oldu.",
|
||||
"Failed to load subscription plans.": "Abonelik planlarını yükleme başarısız oldu.",
|
||||
"year": "yıl"
|
||||
"year": "yıl",
|
||||
"Failed to create checkout session": "Ödeme oturumu oluşturma başarısız oldu"
|
||||
}
|
||||
|
||||
@@ -466,5 +466,6 @@
|
||||
"Failed to restore purchases.": "Не вдалося відновити покупки.",
|
||||
"Failed to manage subscription.": "Не вдалося керувати підпискою.",
|
||||
"Failed to load subscription plans.": "Не вдалося завантажити плани підписки.",
|
||||
"year": "рік"
|
||||
"year": "рік",
|
||||
"Failed to create checkout session": "Не вдалося створити сесію оформлення замовлення"
|
||||
}
|
||||
|
||||
@@ -454,5 +454,6 @@
|
||||
"Failed to restore purchases.": "Khôi phục giao dịch mua không thành công.",
|
||||
"Failed to manage subscription.": "Quản lý đăng ký không thành công.",
|
||||
"Failed to load subscription plans.": "Tải kế hoạch đăng ký không thành công.",
|
||||
"year": "năm"
|
||||
"year": "năm",
|
||||
"Failed to create checkout session": "Không thể tạo phiên làm việc thanh toán"
|
||||
}
|
||||
|
||||
@@ -454,5 +454,6 @@
|
||||
"Failed to restore purchases.": "恢复购买失败。",
|
||||
"Failed to manage subscription.": "管理订阅失败。",
|
||||
"Failed to load subscription plans.": "加载订阅计划失败。",
|
||||
"year": "年"
|
||||
"year": "年",
|
||||
"Failed to create checkout session": "创建结账会话失败"
|
||||
}
|
||||
|
||||
@@ -454,5 +454,6 @@
|
||||
"Failed to restore purchases.": "恢復購買失敗。",
|
||||
"Failed to manage subscription.": "管理訂閱失敗。",
|
||||
"Failed to load subscription plans.": "加載訂閱計劃失敗。",
|
||||
"year": "年"
|
||||
"year": "年",
|
||||
"Failed to create checkout session": "創建結帳會話失敗"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { AppleIAPVerifier } from '@/libs/iap/apple/verifier';
|
||||
import { AppleIAPVerifier, createAppleIAPVerifier } from '@/libs/iap/apple/verifier';
|
||||
|
||||
const SKIP_IAP_API_TESTS = !process.env['ENABLE_IAP_API_TESTS'];
|
||||
const REAL_TEST_DATA = {
|
||||
@@ -26,7 +26,7 @@ describe.skipIf(SKIP_IAP_API_TESTS)('Apple IAP Integration Tests', () => {
|
||||
let verifier: AppleIAPVerifier;
|
||||
|
||||
beforeAll(() => {
|
||||
verifier = new AppleIAPVerifier({
|
||||
verifier = createAppleIAPVerifier({
|
||||
keyId: process.env['APPLE_IAP_KEY_ID']!,
|
||||
issuerId: process.env['APPLE_IAP_ISSUER_ID']!,
|
||||
bundleId: process.env['APPLE_IAP_BUNDLE_ID']!,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { defaultIAPVerifier } from '@/libs/iap/apple/verifier';
|
||||
import { getAppleIAPVerifier } from '@/libs/iap/apple/verifier';
|
||||
import { createSupabaseAdminClient } from '@/utils/supabase';
|
||||
import { validateUserAndToken } from '@/utils/access';
|
||||
import { IAPError } from '@/types/error';
|
||||
@@ -169,6 +169,7 @@ export async function POST(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
const defaultIAPVerifier = getAppleIAPVerifier();
|
||||
const verificationResult = await defaultIAPVerifier.verifyTransaction(originalTransactionId);
|
||||
if (!verificationResult.success) {
|
||||
console.error('Apple verification failed:', verificationResult.error);
|
||||
|
||||
@@ -112,15 +112,27 @@ const ProfilePage = () => {
|
||||
}
|
||||
setLoading(true);
|
||||
const isEmbeddedCheckout = isTauriAppPlatform();
|
||||
const { sessionId, clientSecret, url } = await fetch(WEB_STRIPE_CHECKOUT_URL, {
|
||||
const response = await fetch(WEB_STRIPE_CHECKOUT_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ priceId, embedded: isEmbeddedCheckout }),
|
||||
}).then((res) => res.json());
|
||||
});
|
||||
setLoading(false);
|
||||
if (!response.ok) {
|
||||
console.error('Failed to create Stripe checkout session');
|
||||
posthog.capture('checkout_error', {
|
||||
error: 'Failed to create Stripe checkout session',
|
||||
});
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
message: _('Failed to create checkout session'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { sessionId, clientSecret, url } = await response.json();
|
||||
|
||||
const selectedPlan = availablePlans.find((plan) => plan.price_id === priceId)!;
|
||||
const planName = selectedPlan.product?.name || selectedPlan.productName;
|
||||
@@ -193,6 +205,9 @@ const ProfilePage = () => {
|
||||
try {
|
||||
const purchases = await iapService.restorePurchases();
|
||||
if (purchases.length > 0) {
|
||||
purchases.sort(
|
||||
(a, b) => new Date(a.purchaseDate).getTime() - new Date(b.purchaseDate).getTime(),
|
||||
);
|
||||
const purchase = purchases[0]!;
|
||||
const params = new URLSearchParams({
|
||||
payment: 'iap',
|
||||
@@ -285,7 +300,10 @@ const ProfilePage = () => {
|
||||
} else {
|
||||
fetch(WEB_STRIPE_PLANS_URL)
|
||||
.then((res) => res.json())
|
||||
.then((data) => setAvailablePlans(data || []));
|
||||
.then((data) => {
|
||||
const availablePlans = data && data instanceof Array ? data : [];
|
||||
setAvailablePlans(availablePlans);
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appService]);
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { getAPIBaseUrl } from '@/services/environment';
|
||||
import { getAPIBaseUrl, getNodeAPIBaseUrl } from '@/services/environment';
|
||||
import { getAccessToken } from '@/utils/access';
|
||||
import { supabase } from '@/utils/supabase';
|
||||
import Spinner from '@/components/Spinner';
|
||||
|
||||
const WEB_STRIPE_CHECK_URL = `${getAPIBaseUrl()}/stripe/check`;
|
||||
const WEB_APPLE_IAP_VERIFY_URL = `${getAPIBaseUrl()}/apple/iap-verify`;
|
||||
const STRIPE_CHECK_URL = `${getAPIBaseUrl()}/stripe/check`;
|
||||
const APPLE_IAP_VERIFY_URL = `${getNodeAPIBaseUrl()}/apple/iap-verify`;
|
||||
|
||||
interface SessionStatus {
|
||||
status: 'loading' | 'complete' | 'failed' | 'processing';
|
||||
@@ -40,7 +40,7 @@ const SuccessPageWithSearchParams = () => {
|
||||
const updateStripeSessionStatus = async () => {
|
||||
try {
|
||||
const token = await getAccessToken();
|
||||
const response = await fetch(WEB_STRIPE_CHECK_URL, {
|
||||
const response = await fetch(STRIPE_CHECK_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -93,7 +93,7 @@ const SuccessPageWithSearchParams = () => {
|
||||
if (platform === 'ios') {
|
||||
try {
|
||||
const token = await getAccessToken();
|
||||
const response = await fetch(WEB_APPLE_IAP_VERIFY_URL, {
|
||||
const response = await fetch(APPLE_IAP_VERIFY_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -118,13 +118,19 @@ export class AppleIAPVerifier {
|
||||
|
||||
export const createAppleIAPVerifier = (config: AppleIAPConfig) => new AppleIAPVerifier(config);
|
||||
|
||||
export const defaultIAPVerifier = new AppleIAPVerifier({
|
||||
keyId: process.env['APPLE_IAP_KEY_ID']!,
|
||||
issuerId: process.env['APPLE_IAP_ISSUER_ID']!,
|
||||
bundleId: process.env['APPLE_IAP_BUNDLE_ID']!,
|
||||
privateKey: Buffer.from(process.env['APPLE_IAP_PRIVATE_KEY_BASE64']! || '', 'base64').toString(
|
||||
'utf-8',
|
||||
),
|
||||
environment: 'sandbox',
|
||||
// environment: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
|
||||
});
|
||||
let defaultIAPVerifier: AppleIAPVerifier | undefined;
|
||||
export const getAppleIAPVerifier = () => {
|
||||
if (!defaultIAPVerifier) {
|
||||
defaultIAPVerifier = createAppleIAPVerifier({
|
||||
keyId: process.env['APPLE_IAP_KEY_ID']!,
|
||||
issuerId: process.env['APPLE_IAP_ISSUER_ID']!,
|
||||
bundleId: process.env['APPLE_IAP_BUNDLE_ID']!,
|
||||
privateKey: Buffer.from(
|
||||
process.env['APPLE_IAP_PRIVATE_KEY_BASE64']! || '',
|
||||
'base64',
|
||||
).toString('utf-8'),
|
||||
environment: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
|
||||
});
|
||||
}
|
||||
return defaultIAPVerifier;
|
||||
};
|
||||
|
||||
@@ -535,6 +535,7 @@ export const BOOK_IDS_SEPARATOR = '+';
|
||||
export const DOWNLOAD_READEST_URL = 'https://readest.com?utm_source=readest_web';
|
||||
|
||||
export const READEST_WEB_BASE_URL = 'https://web.readest.com';
|
||||
export const READEST_NODE_BASE_URL = 'https://node.readest.com';
|
||||
|
||||
const LATEST_DOWNLOAD_BASE_URL = 'https://download.readest.com/releases';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AppService } from '@/types/system';
|
||||
import { READEST_WEB_BASE_URL } from './constants';
|
||||
import { READEST_NODE_BASE_URL, READEST_WEB_BASE_URL } from './constants';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -12,12 +12,18 @@ export const isWebAppPlatform = () => process.env['NEXT_PUBLIC_APP_PLATFORM'] ==
|
||||
export const hasCli = () => window.__READEST_CLI_ACCESS === true;
|
||||
export const isPWA = () => window.matchMedia('(display-mode: standalone)').matches;
|
||||
export const getBaseUrl = () => process.env['NEXT_PUBLIC_API_BASE_URL'] ?? READEST_WEB_BASE_URL;
|
||||
export const getNodeBaseUrl = () =>
|
||||
process.env['NEXT_PUBLIC_NODE_BASE_URL'] ?? READEST_NODE_BASE_URL;
|
||||
|
||||
const isWebDevMode = () => process.env['NODE_ENV'] === 'development' && isWebAppPlatform();
|
||||
|
||||
// Dev API only in development mode and web platform
|
||||
// with command `pnpm dev-web`
|
||||
// for production build or tauri app use the production Web API
|
||||
export const getAPIBaseUrl = () =>
|
||||
process.env['NODE_ENV'] === 'development' && isWebAppPlatform() ? '/api' : `${getBaseUrl()}/api`;
|
||||
export const getAPIBaseUrl = () => (isWebDevMode() ? '/api' : `${getBaseUrl()}/api`);
|
||||
|
||||
// For Node.js API that currently not supported in some edge runtimes
|
||||
export const getNodeAPIBaseUrl = () => (isWebDevMode() ? '/api' : `${getNodeBaseUrl()}/api`);
|
||||
|
||||
export interface EnvConfigType {
|
||||
getAppService: () => Promise<AppService>;
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
],
|
||||
"@pdfjs/*": [
|
||||
"./public/vendor/pdfjs/*"
|
||||
]
|
||||
],
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
Generated
+327
-140
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user