Add Stripe checkout/portal integration and align subscription types with API

- Add Stripe Checkout session and Customer Portal API calls and mutation hooks
- Add success/cancel redirect pages for Stripe Checkout flow
- Update subscription status component with upgrade buttons, manage subscription, and trial badge
- Align subscription types (limits, feature benefits, upgrade triggers) with current API response shape
- Update demo provider data to match new type contracts
- Rename "Premium" tier references to "Pro" throughout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-07 05:58:49 -06:00
parent 9ac3434b06
commit 4b8c10d768
8 changed files with 341 additions and 107 deletions
+70 -19
View File
@@ -25,37 +25,46 @@ export interface SubscriptionResponse {
export interface SubscriptionStatusResponse {
tier: string;
status: string;
is_active: boolean;
subscribed_at?: string;
expires_at?: string;
limits: TierLimitsResponse;
auto_renew: boolean;
trial_start?: string | null;
trial_end?: string | null;
trial_active?: boolean;
subscription_source?: string;
usage?: UsageResponse | null;
limits: Record<string, TierLimitsResponse>;
limitations_enabled: boolean;
}
export interface TierLimitsResponse {
max_residences: number;
max_tasks_per_residence: number;
max_contractors: number;
max_documents: number;
can_share: boolean;
can_export: boolean;
properties: number | null;
tasks: number | null;
contractors: number | null;
documents: number | null;
}
export interface UsageResponse {
residences?: number;
tasks?: number;
contractors?: number;
documents?: number;
}
export interface UpgradeTriggerResponse {
id: number;
key: string;
trigger_key: string;
title: string;
description: string;
action_text: string;
icon: string;
message: string;
promo_html: string;
button_text: string;
}
export interface FeatureBenefitResponse {
id: number;
title: string;
description: string;
icon: string;
tier: string;
sort_order: number;
feature_name: string;
free_tier_text: string;
pro_tier_text: string;
display_order: number;
}
export interface PromotionResponse {
@@ -151,3 +160,45 @@ export function restoreSubscription(
body: JSON.stringify(data),
});
}
// ---------------------------------------------------------------------------
// Stripe Checkout / Portal
// ---------------------------------------------------------------------------
export interface CheckoutSessionRequest {
price_id: string;
success_url: string;
cancel_url: string;
}
export interface CheckoutSessionResponse {
checkout_url: string;
}
export interface PortalSessionRequest {
return_url: string;
}
export interface PortalSessionResponse {
portal_url: string;
}
/** Create a Stripe Checkout session and return the checkout URL. */
export function createCheckoutSession(
data: CheckoutSessionRequest,
): Promise<CheckoutSessionResponse> {
return apiFetch<CheckoutSessionResponse>('/subscription/checkout/', {
method: 'POST',
body: JSON.stringify(data),
});
}
/** Create a Stripe Customer Portal session and return the portal URL. */
export function createPortalSession(
data: PortalSessionRequest,
): Promise<PortalSessionResponse> {
return apiFetch<PortalSessionResponse>('/subscription/portal/', {
method: 'POST',
body: JSON.stringify(data),
});
}
+10 -13
View File
@@ -325,26 +325,23 @@ export const demoProvider: DataProvider = {
subscription: {
getStatus: async () => ({
tier: 'free',
status: 'active',
is_active: true,
auto_renew: false,
limits: {
max_residences: 2,
max_tasks_per_residence: 25,
max_contractors: 10,
max_documents: 20,
can_share: false,
can_export: false,
free: { properties: 2, tasks: 25, contractors: 10, documents: 20 },
pro: { properties: null, tasks: null, contractors: null, documents: null },
},
limitations_enabled: true,
}),
getFeatureBenefits: async () => [
{ id: 1, title: 'Unlimited Residences', description: 'Track all your properties', icon: '🏠', tier: 'pro', sort_order: 1 },
{ id: 2, title: 'Unlimited Tasks', description: 'Never hit a task limit', icon: '✅', tier: 'pro', sort_order: 2 },
{ id: 3, title: 'Sharing', description: 'Share residences with family', icon: '👥', tier: 'pro', sort_order: 3 },
{ id: 4, title: 'PDF Reports', description: 'Export task reports', icon: '📄', tier: 'pro', sort_order: 4 },
{ feature_name: 'Unlimited Residences', free_tier_text: 'Up to 2', pro_tier_text: 'Unlimited', display_order: 1 },
{ feature_name: 'Unlimited Tasks', free_tier_text: 'Up to 25', pro_tier_text: 'Unlimited', display_order: 2 },
{ feature_name: 'Sharing', free_tier_text: 'No', pro_tier_text: 'Yes', display_order: 3 },
{ feature_name: 'PDF Reports', free_tier_text: 'No', pro_tier_text: 'Yes', display_order: 4 },
],
getUpgradeTriggers: async () => [
{ id: 1, key: 'residence_limit', title: 'Need more properties?', description: 'Upgrade to track unlimited residences.', action_text: 'Upgrade', icon: '🏠' },
{ id: 2, key: 'sharing', title: 'Want to share?', description: 'Upgrade to share residences with family members.', action_text: 'Upgrade', icon: '👥' },
{ trigger_key: 'residence_limit', title: 'Need more properties?', message: 'Upgrade to track unlimited residences.', promo_html: '', button_text: 'Upgrade' },
{ trigger_key: 'sharing', title: 'Want to share?', message: 'Upgrade to share residences with family members.', promo_html: '', button_text: 'Upgrade' },
],
},
+35 -1
View File
@@ -1,7 +1,15 @@
"use client";
import { useQuery } from '@tanstack/react-query';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context';
import {
createCheckoutSession,
createPortalSession,
} from '@/lib/api/subscription';
import type {
CheckoutSessionRequest,
PortalSessionRequest,
} from '@/lib/api/subscription';
export function useSubscriptionStatus() {
const { subscription } = useDataProvider();
@@ -31,3 +39,29 @@ export function useUpgradeTriggers() {
staleTime: Infinity,
});
}
// ---------------------------------------------------------------------------
// Mutation hooks (Stripe Checkout / Portal)
// ---------------------------------------------------------------------------
export function useCreateCheckoutSession() {
const queryClient = useQueryClient();
const qk = useQueryKeyPrefix();
return useMutation({
mutationFn: (data: CheckoutSessionRequest) => createCheckoutSession(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: qk('subscription') });
},
});
}
export function useCreatePortalSession() {
const queryClient = useQueryClient();
const qk = useQueryKeyPrefix();
return useMutation({
mutationFn: (data: PortalSessionRequest) => createPortalSession(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: qk('subscription') });
},
});
}
+28
View File
@@ -62,12 +62,18 @@ export interface TierLimitsClientResponse {
}
export interface SubscriptionStatusResponse {
tier: string;
is_active: boolean;
subscribed_at: string | null;
expires_at: string | null;
auto_renew: boolean;
usage: UsageResponse | null;
limits: Record<string, TierLimitsClientResponse>;
limitations_enabled: boolean;
trial_start?: string | null;
trial_end?: string | null;
trial_active?: boolean;
subscription_source?: string;
}
export interface UpgradeTriggerResponse {
@@ -101,6 +107,28 @@ export interface PromotionResponse {
end_date: string;
}
// ---------------------------------------------------------------------------
// Stripe Checkout / Portal
// ---------------------------------------------------------------------------
export interface CheckoutSessionRequest {
price_id: string;
success_url: string;
cancel_url: string;
}
export interface CheckoutSessionResponse {
checkout_url: string;
}
export interface PortalSessionRequest {
return_url: string;
}
export interface PortalSessionResponse {
portal_url: string;
}
// ---------------------------------------------------------------------------
// Purchase response wrappers (inline maps from handler)
// ---------------------------------------------------------------------------