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:
+70
-19
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -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') });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user