diff --git a/VERSION b/VERSION index 818944f5b..236c7ad08 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.22 +0.0.21 diff --git a/surfsense_backend/app/app.py b/surfsense_backend/app/app.py index 5057e7d00..08194e7fb 100644 --- a/surfsense_backend/app/app.py +++ b/surfsense_backend/app/app.py @@ -754,12 +754,6 @@ app.add_middleware( allow_credentials=True, allow_methods=["*"], # Allows all methods allow_headers=["*"], # Allows all headers - # Cache CORS preflight (OPTIONS) responses for 24h. Browsers clamp: - # Chrome/Edge cap at 7200s, Firefox honours up to 86400s. Setting the - # higher value lets each browser cache for as long as it allows. This - # eliminates an OPTIONS round-trip on every non-simple request from - # FRONTEND_URL to BACKEND_URL. - max_age=86400, ) # Password / email-based auth routers are only mounted when not running in diff --git a/surfsense_backend/app/routes/stripe_routes.py b/surfsense_backend/app/routes/stripe_routes.py index fc5fded84..aed74ec8d 100644 --- a/surfsense_backend/app/routes/stripe_routes.py +++ b/surfsense_backend/app/routes/stripe_routes.py @@ -26,7 +26,6 @@ from app.schemas.stripe import ( CreateCheckoutSessionResponse, CreateTokenCheckoutSessionRequest, CreateTokenCheckoutSessionResponse, - FinalizeCheckoutResponse, PagePurchaseHistoryResponse, StripeStatusResponse, StripeWebhookResponse, @@ -66,15 +65,7 @@ def _get_checkout_urls(search_space_id: int) -> tuple[str, str]: ) base_url = config.NEXT_FRONTEND_URL.rstrip("/") - # Stripe substitutes ``{CHECKOUT_SESSION_ID}`` with the actual session id - # at redirect time. The frontend uses it to call /stripe/finalize-checkout - # which fulfils synchronously without waiting for the webhook — fixing the - # webhook-vs-redirect race where users land on /purchase-success before - # checkout.session.completed has been delivered. - success_url = ( - f"{base_url}/dashboard/{search_space_id}/purchase-success" - f"?session_id={{CHECKOUT_SESSION_ID}}" - ) + success_url = f"{base_url}/dashboard/{search_space_id}/purchase-success" cancel_url = f"{base_url}/dashboard/{search_space_id}/purchase-cancel" return success_url, cancel_url @@ -97,62 +88,10 @@ def _normalize_optional_string(value: Any) -> str | None: def _get_metadata(checkout_session: Any) -> dict[str, str]: - """Extract checkout session metadata as a plain ``str -> str`` dict. - - In ``stripe>=15.0`` ``StripeObject`` is no longer a ``dict`` subclass - and exposes neither ``items()`` nor ``__iter__`` nor ``keys()``. - ``dict(obj)`` falls into the sequence protocol and raises - ``KeyError: 0``; ``obj.items()`` raises ``AttributeError``. The - supported way to materialize a ``StripeObject`` as a plain dict is - its ``to_dict()`` method (added in stripe-python 8.x, present in 15.x). - """ - metadata = getattr(checkout_session, "metadata", None) - if metadata is None: - return {} - - # 1. Plain dict (older SDKs that subclassed dict, JSON-decoded events - # in tests, etc.). + metadata = getattr(checkout_session, "metadata", None) or {} if isinstance(metadata, dict): - return {str(k): str(v) for k, v in metadata.items()} - - # 2. Modern Stripe SDK: every ``StripeObject`` has ``to_dict()``. - # ``recursive=False`` is correct because Stripe metadata values - # are always primitive strings. - to_dict = getattr(metadata, "to_dict", None) - if callable(to_dict): - try: - d = to_dict(recursive=False) - if isinstance(d, dict): - return {str(k): str(v) for k, v in d.items()} - except Exception: - logger.exception( - "Stripe metadata.to_dict() failed for session %s", - getattr(checkout_session, "id", "?"), - ) - - # 3. Last-resort: read the SDK's private ``_data`` backing dict. - # Stable across stripe-python 6.x -> 15.x. - inner = getattr(metadata, "_data", None) - if isinstance(inner, dict): - return {str(k): str(v) for k, v in inner.items()} - - logger.warning( - "Could not extract metadata from checkout session %s (metadata type=%s)", - getattr(checkout_session, "id", "?"), - type(metadata).__name__, - ) - return {} - - -# Canonical purchase_type metadata values. ``premium_credit`` was emitted -# by an earlier release of ``create_token_checkout_session`` so it's still -# accepted on the read side for backward compat with in-flight sessions. -_PURCHASE_TYPE_TOKEN_VALUES = frozenset({"premium_tokens", "premium_credit"}) - - -def _is_token_purchase(metadata: dict[str, str]) -> bool: - """Return True for premium-credit (a.k.a. premium_token) purchases.""" - return metadata.get("purchase_type", "page_packs") in _PURCHASE_TYPE_TOKEN_VALUES + return {str(key): str(value) for key, value in metadata.items()} + return dict(metadata) async def _get_or_create_purchase_from_checkout_session( @@ -500,217 +439,45 @@ async def stripe_webhook( detail="Invalid Stripe webhook signature.", ) from exc - try: - if event.type in { - "checkout.session.completed", - "checkout.session.async_payment_succeeded", + if event.type in { + "checkout.session.completed", + "checkout.session.async_payment_succeeded", + }: + checkout_session = event.data.object + payment_status = getattr(checkout_session, "payment_status", None) + + if event.type == "checkout.session.completed" and payment_status not in { + "paid", + "no_payment_required", }: - checkout_session = event.data.object - payment_status = getattr(checkout_session, "payment_status", None) + logger.info( + "Received checkout.session.completed for unpaid session %s; waiting for async success.", + checkout_session.id, + ) + return StripeWebhookResponse() - if event.type == "checkout.session.completed" and payment_status not in { - "paid", - "no_payment_required", - }: - logger.info( - "Received checkout.session.completed for unpaid session %s; waiting for async success.", - checkout_session.id, - ) - return StripeWebhookResponse() + metadata = _get_metadata(checkout_session) + purchase_type = metadata.get("purchase_type", "page_packs") + if purchase_type == "premium_tokens": + return await _fulfill_completed_token_purchase(db_session, checkout_session) + return await _fulfill_completed_purchase(db_session, checkout_session) - metadata = _get_metadata(checkout_session) - if _is_token_purchase(metadata): - return await _fulfill_completed_token_purchase( - db_session, checkout_session - ) - return await _fulfill_completed_purchase(db_session, checkout_session) - - if event.type in { - "checkout.session.async_payment_failed", - "checkout.session.expired", - }: - checkout_session = event.data.object - metadata = _get_metadata(checkout_session) - if _is_token_purchase(metadata): - return await _mark_token_purchase_failed( - db_session, str(checkout_session.id) - ) - return await _mark_purchase_failed(db_session, str(checkout_session.id)) - except Exception: - # Re-raise so FastAPI returns 500 and Stripe retries this delivery. - # Logging here gives us a structured trail with event id + type so - # future webhook bugs surface immediately in the logs without - # having to grep by request_id. - logger.exception( - "Stripe webhook handler failed for event id=%s type=%s — Stripe will retry", - getattr(event, "id", "?"), - getattr(event, "type", "?"), - ) - raise + if event.type in { + "checkout.session.async_payment_failed", + "checkout.session.expired", + }: + checkout_session = event.data.object + metadata = _get_metadata(checkout_session) + purchase_type = metadata.get("purchase_type", "page_packs") + if purchase_type == "premium_tokens": + return await _mark_token_purchase_failed( + db_session, str(checkout_session.id) + ) + return await _mark_purchase_failed(db_session, str(checkout_session.id)) return StripeWebhookResponse() -@router.get("/finalize-checkout", response_model=FinalizeCheckoutResponse) -async def finalize_checkout( - session_id: str, - user: User = Depends(current_active_user), - db_session: AsyncSession = Depends(get_async_session), -) -> FinalizeCheckoutResponse: - """Synchronously fulfil a checkout session from the success page. - - Solves the webhook-vs-redirect race: the user lands on - ``/dashboard//purchase-success?session_id=cs_...`` typically a - few hundred ms after paying, but Stripe's - ``checkout.session.completed`` webhook can take 5-30s+ to arrive. - Calling this endpoint on success-page mount fulfils the purchase - immediately by retrieving the session from Stripe's API and - invoking the same idempotent helpers the webhook uses. - - Idempotency: if the webhook has already fulfilled this purchase - (status=COMPLETED), the helpers short-circuit and we just return - the latest balance. Concurrent webhook + finalize calls are safe - because both acquire ``SELECT ... FOR UPDATE`` on the purchase row. - - Authorization: the session's ``client_reference_id`` must match the - authenticated user's id. This prevents a user from finalising - someone else's checkout session if they happen to know the id. - """ - stripe_client = get_stripe_client() - - try: - checkout_session = stripe_client.v1.checkout.sessions.retrieve(session_id) - except StripeError as exc: - logger.warning( - "finalize_checkout: stripe lookup failed for session=%s user=%s: %s", - session_id, - user.id, - exc, - ) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Checkout session not found.", - ) from exc - - # Authorization check: the user finalising must be the user who - # initiated the checkout. ``client_reference_id`` is set in - # ``create_checkout_session`` / ``create_token_checkout_session``. - client_reference_id = getattr(checkout_session, "client_reference_id", None) - if client_reference_id != str(user.id): - logger.warning( - "finalize_checkout: ownership mismatch session=%s client_ref=%s user=%s", - session_id, - client_reference_id, - user.id, - ) - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="This checkout session does not belong to you.", - ) - - metadata = _get_metadata(checkout_session) - is_token = _is_token_purchase(metadata) - payment_status = getattr(checkout_session, "payment_status", None) - session_status = getattr(checkout_session, "status", None) - - # Defensive fallback: if metadata can't be read for any reason - # (extraction failure, manually-created session in Stripe dashboard, - # SDK upgrade breaking ``to_dict``, etc.) we'd otherwise route every - # purchase to the page_packs handler and get stuck. Resolve the - # purchase_type by checking which table actually has the row keyed - # by this Stripe session id. - if not metadata: - existing_token_purchase = ( - await db_session.execute( - select(PremiumTokenPurchase.id).where( - PremiumTokenPurchase.stripe_checkout_session_id - == str(checkout_session.id) - ) - ) - ).scalar_one_or_none() - if existing_token_purchase is not None: - is_token = True - else: - existing_page_purchase = ( - await db_session.execute( - select(PagePurchase.id).where( - PagePurchase.stripe_checkout_session_id - == str(checkout_session.id) - ) - ) - ).scalar_one_or_none() - if existing_page_purchase is None: - logger.error( - "finalize_checkout: no purchase row in either table " - "and metadata is empty for session=%s user=%s", - session_id, - user.id, - ) - # Fall through; downstream path will short-circuit on - # missing-row + empty-metadata. - logger.info( - "finalize_checkout: recovered purchase_type=%s for session=%s " - "via DB fallback (metadata was empty)", - "premium_tokens" if is_token else "page_packs", - session_id, - ) - - is_paid = payment_status in {"paid", "no_payment_required"} - is_expired = session_status == "expired" - - if is_paid: - if is_token: - await _fulfill_completed_token_purchase(db_session, checkout_session) - else: - await _fulfill_completed_purchase(db_session, checkout_session) - elif is_expired: - if is_token: - await _mark_token_purchase_failed(db_session, str(checkout_session.id)) - else: - await _mark_purchase_failed(db_session, str(checkout_session.id)) - # Otherwise (e.g. payment_status="unpaid", session_status="open"), - # leave the purchase row alone — frontend will keep polling and the - # webhook will eventually win the race. - - # Refresh the user row so the response reflects any update applied - # by the fulfilment helpers in this same session. - await db_session.refresh(user) - - if is_token: - purchase = ( - await db_session.execute( - select(PremiumTokenPurchase).where( - PremiumTokenPurchase.stripe_checkout_session_id - == str(checkout_session.id) - ) - ) - ).scalar_one_or_none() - return FinalizeCheckoutResponse( - purchase_type="premium_tokens", - status=purchase.status.value if purchase else "pending", - premium_credit_micros_limit=user.premium_credit_micros_limit, - premium_credit_micros_used=user.premium_credit_micros_used, - premium_credit_micros_granted=( - purchase.credit_micros_granted if purchase else None - ), - ) - - purchase = ( - await db_session.execute( - select(PagePurchase).where( - PagePurchase.stripe_checkout_session_id == str(checkout_session.id) - ) - ) - ).scalar_one_or_none() - return FinalizeCheckoutResponse( - purchase_type="page_packs", - status=purchase.status.value if purchase else "pending", - pages_limit=user.pages_limit, - pages_used=user.pages_used, - pages_granted=purchase.pages_granted if purchase else None, - ) - - @router.get("/purchases", response_model=PagePurchaseHistoryResponse) async def get_page_purchases( user: User = Depends(current_active_user), @@ -757,11 +524,7 @@ def _get_token_checkout_urls(search_space_id: int) -> tuple[str, str]: detail="NEXT_FRONTEND_URL is not configured.", ) base_url = config.NEXT_FRONTEND_URL.rstrip("/") - # See ``_get_checkout_urls`` for why session_id is appended. - success_url = ( - f"{base_url}/dashboard/{search_space_id}/purchase-success" - f"?session_id={{CHECKOUT_SESSION_ID}}" - ) + success_url = f"{base_url}/dashboard/{search_space_id}/purchase-success" cancel_url = f"{base_url}/dashboard/{search_space_id}/purchase-cancel" return success_url, cancel_url @@ -812,11 +575,7 @@ async def create_token_checkout_session( "user_id": str(user.id), "quantity": str(body.quantity), "credit_micros_per_unit": str(config.STRIPE_CREDIT_MICROS_PER_UNIT), - # Canonical value matched by ``_is_token_purchase``. - # The legacy ``"premium_credit"`` is still accepted on - # the read side for any in-flight sessions started - # before this rename. - "purchase_type": "premium_tokens", + "purchase_type": "premium_credit", }, } ) diff --git a/surfsense_backend/app/schemas/stripe.py b/surfsense_backend/app/schemas/stripe.py index ad13ddf04..57265ec8e 100644 --- a/surfsense_backend/app/schemas/stripe.py +++ b/surfsense_backend/app/schemas/stripe.py @@ -56,26 +56,6 @@ class StripeWebhookResponse(BaseModel): received: bool = True -class FinalizeCheckoutResponse(BaseModel): - """Response from /stripe/finalize-checkout. - - Returned by the success page so the UI can show the post-purchase - balance immediately, even when the Stripe webhook hasn't been - delivered yet. ``status`` mirrors the underlying purchase row - (``pending`` / ``completed`` / ``failed``); the FE polls this - endpoint until it sees ``completed`` or a final ``failed``. - """ - - purchase_type: str # "page_packs" | "premium_tokens" - status: str # PagePurchaseStatus / PremiumTokenPurchaseStatus value - pages_limit: int | None = None - pages_used: int | None = None - pages_granted: int | None = None - premium_credit_micros_limit: int | None = None - premium_credit_micros_used: int | None = None - premium_credit_micros_granted: int | None = None - - class CreateTokenCheckoutSessionRequest(BaseModel): """Request body for creating a premium token purchase checkout session.""" diff --git a/surfsense_backend/pyproject.toml b/surfsense_backend/pyproject.toml index 4235ac962..da8c4b7d1 100644 --- a/surfsense_backend/pyproject.toml +++ b/surfsense_backend/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "surf-new-backend" -version = "0.0.22" +version = "0.0.21" description = "SurfSense Backend" requires-python = ">=3.12" dependencies = [ diff --git a/surfsense_backend/uv.lock b/surfsense_backend/uv.lock index 4dd5156e7..3e371cecc 100644 --- a/surfsense_backend/uv.lock +++ b/surfsense_backend/uv.lock @@ -7947,7 +7947,7 @@ wheels = [ [[package]] name = "surf-new-backend" -version = "0.0.22" +version = "0.0.21" source = { editable = "." } dependencies = [ { name = "alembic" }, diff --git a/surfsense_browser_extension/package.json b/surfsense_browser_extension/package.json index b8b5cb2ec..f127b85c0 100644 --- a/surfsense_browser_extension/package.json +++ b/surfsense_browser_extension/package.json @@ -1,7 +1,7 @@ { "name": "surfsense_browser_extension", "displayName": "Surfsense Browser Extension", - "version": "0.0.22", + "version": "0.0.21", "description": "Extension to collect Browsing History for SurfSense.", "author": "https://github.com/MODSetter", "engines": { diff --git a/surfsense_desktop/package.json b/surfsense_desktop/package.json index 744ab65ab..4826b904e 100644 --- a/surfsense_desktop/package.json +++ b/surfsense_desktop/package.json @@ -1,6 +1,6 @@ { "name": "surfsense-desktop", - "version": "0.0.22", + "version": "0.0.21", "description": "SurfSense Desktop App", "main": "dist/main.js", "scripts": { diff --git a/surfsense_web/app/dashboard/[search_space_id]/purchase-success/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/purchase-success/page.tsx index b3d504ed5..85bc4aaa6 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/purchase-success/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/purchase-success/page.tsx @@ -1,9 +1,8 @@ "use client"; -import { AlertCircle, CheckCircle2, Loader2 } from "lucide-react"; +import { CheckCircle2 } from "lucide-react"; import Link from "next/link"; -import { useParams, useSearchParams } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; +import { useParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Card, @@ -13,133 +12,23 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import type { FinalizeCheckoutResponse } from "@/contracts/types/stripe.types"; -import { stripeApiService } from "@/lib/apis/stripe-api.service"; - -type FinalizeState = - | { kind: "loading" } - | { kind: "completed"; data: FinalizeCheckoutResponse } - | { kind: "pending"; data: FinalizeCheckoutResponse } - | { kind: "still_pending"; data: FinalizeCheckoutResponse } - | { kind: "failed"; data: FinalizeCheckoutResponse } - | { kind: "error"; message: string } - | { kind: "no_session" }; - -const POLL_INTERVAL_MS = 2000; -const MAX_POLL_ATTEMPTS = 15; // ~30s total before falling back to the still_pending state export default function PurchaseSuccessPage() { const params = useParams(); - const searchParams = useSearchParams(); const searchSpaceId = String(params.search_space_id ?? ""); - const sessionId = searchParams.get("session_id"); - - const [state, setState] = useState( - sessionId ? { kind: "loading" } : { kind: "no_session" } - ); - // Tracks active polling so component unmount cancels it - const cancelledRef = useRef(false); - - useEffect(() => { - if (!sessionId) return; - - cancelledRef.current = false; - - const poll = async (attempt: number): Promise => { - if (cancelledRef.current) return; - try { - const data = await stripeApiService.finalizeCheckout(sessionId); - if (cancelledRef.current) return; - - if (data.status === "completed") { - setState({ kind: "completed", data }); - return; - } - if (data.status === "failed") { - setState({ kind: "failed", data }); - return; - } - - // Status is "pending" - either the user paid via async - // payment method (Klarna, ACH) or webhook + finalize both - // raced and lost. Keep polling up to MAX_POLL_ATTEMPTS, - // then fall back to a friendlier message that explains - // fulfilment may complete asynchronously. - if (attempt < MAX_POLL_ATTEMPTS) { - setState({ kind: "pending", data }); - setTimeout(() => poll(attempt + 1), POLL_INTERVAL_MS); - } else { - setState({ kind: "still_pending", data }); - } - } catch (err) { - if (cancelledRef.current) return; - const message = err instanceof Error ? err.message : "Unable to finalize checkout."; - setState({ kind: "error", message }); - } - }; - - void poll(1); - - return () => { - cancelledRef.current = true; - }; - }, [sessionId]); return (
- {state.kind === "loading" || state.kind === "pending" ? ( - - ) : state.kind === "completed" ? ( - - ) : ( - - )} - - {state.kind === "loading" && "Confirming payment…"} - {state.kind === "pending" && "Processing your payment…"} - {state.kind === "still_pending" && "Payment still processing"} - {state.kind === "completed" && "Purchase complete"} - {state.kind === "failed" && "Purchase failed"} - {state.kind === "error" && "Couldn't confirm payment"} - {state.kind === "no_session" && "Purchase complete"} - - - {state.kind === "loading" && "We're verifying your payment with Stripe."} - {state.kind === "pending" && - "Your bank is taking a moment to confirm. This usually takes 5–30 seconds."} - {state.kind === "still_pending" && - "Your payment is still being processed by your bank. We'll apply your purchase as soon as it clears — usually within a few minutes. You can safely close this page."} - {state.kind === "completed" && - (state.data.purchase_type === "page_packs" - ? `Added ${formatNumber(state.data.pages_granted ?? 0)} pages to your account.` - : `Added ${formatCredit(state.data.premium_credit_micros_granted ?? 0)} of premium credit to your account.`)} - {state.kind === "failed" && - "Stripe reported the checkout as failed or expired. Your card was not charged."} - {state.kind === "error" && - "Don't worry — if your card was charged, your purchase will still apply within a minute or two."} - {state.kind === "no_session" && - "Your purchase is being applied to your account."} - + + Purchase complete + Your purchase is being applied to your account now. - {state.kind === "completed" && state.data.purchase_type === "page_packs" && ( -

- New balance: {formatNumber(state.data.pages_limit ?? 0)} total pages - {typeof state.data.pages_used === "number" - ? ` (${formatNumber((state.data.pages_limit ?? 0) - state.data.pages_used)} remaining)` - : ""} -

- )} - {state.kind === "completed" && state.data.purchase_type === "premium_tokens" && ( -

- New premium credit balance: {formatCredit(state.data.premium_credit_micros_limit ?? 0)} -

- )} - {state.kind === "error" && ( -

{state.message}

- )} +

+ Your usage meters should refresh automatically in a moment. +

); } - -function formatNumber(n: number): string { - return new Intl.NumberFormat("en-US").format(n); -} - -function formatCredit(micros: number): string { - const dollars = micros / 1_000_000; - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - maximumFractionDigits: 2, - }).format(dollars); -} diff --git a/surfsense_web/contracts/types/stripe.types.ts b/surfsense_web/contracts/types/stripe.types.ts index 35ec0cb17..251f7a176 100644 --- a/surfsense_web/contracts/types/stripe.types.ts +++ b/surfsense_web/contracts/types/stripe.types.ts @@ -73,19 +73,6 @@ export const getTokenPurchasesResponse = z.object({ purchases: z.array(tokenPurchase), }); -// Response from /stripe/finalize-checkout. Either page or token fields -// are populated depending on purchase_type. -export const finalizeCheckoutResponse = z.object({ - purchase_type: z.enum(["page_packs", "premium_tokens"]), - status: pagePurchaseStatusEnum, - pages_limit: z.number().nullable().optional(), - pages_used: z.number().nullable().optional(), - pages_granted: z.number().nullable().optional(), - premium_credit_micros_limit: z.number().nullable().optional(), - premium_credit_micros_used: z.number().nullable().optional(), - premium_credit_micros_granted: z.number().nullable().optional(), -}); - export type PagePurchaseStatus = z.infer; export type CreateCheckoutSessionRequest = z.infer; export type CreateCheckoutSessionResponse = z.infer; @@ -98,4 +85,3 @@ export type TokenStripeStatusResponse = z.infer; export type TokenPurchase = z.infer; export type GetTokenPurchasesResponse = z.infer; -export type FinalizeCheckoutResponse = z.infer; diff --git a/surfsense_web/lib/apis/stripe-api.service.ts b/surfsense_web/lib/apis/stripe-api.service.ts index f119fbf6a..6e74d7edc 100644 --- a/surfsense_web/lib/apis/stripe-api.service.ts +++ b/surfsense_web/lib/apis/stripe-api.service.ts @@ -5,8 +5,6 @@ import { type CreateTokenCheckoutSessionResponse, createCheckoutSessionResponse, createTokenCheckoutSessionResponse, - type FinalizeCheckoutResponse, - finalizeCheckoutResponse, type GetPagePurchasesResponse, type GetTokenPurchasesResponse, getPagePurchasesResponse, @@ -56,20 +54,6 @@ class StripeApiService { getTokenPurchases = async (): Promise => { return baseApiService.get("/api/v1/stripe/token-purchases", getTokenPurchasesResponse); }; - - /** - * Synchronously fulfil a checkout session from the success page. - * - * Solves the race where the user lands on /purchase-success before - * Stripe's checkout.session.completed webhook arrives. Idempotent — - * safe to call concurrently with the webhook. - */ - finalizeCheckout = async (sessionId: string): Promise => { - return baseApiService.get( - `/api/v1/stripe/finalize-checkout?session_id=${encodeURIComponent(sessionId)}`, - finalizeCheckoutResponse - ); - }; } export const stripeApiService = new StripeApiService(); diff --git a/surfsense_web/package.json b/surfsense_web/package.json index 2adec8638..a34e8a269 100644 --- a/surfsense_web/package.json +++ b/surfsense_web/package.json @@ -1,6 +1,6 @@ { "name": "surfsense_web", - "version": "0.0.22", + "version": "0.0.21", "private": true, "description": "SurfSense Frontend", "scripts": {