diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 4706d2e..6ae6c4a 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -16,6 +16,7 @@ export * from "./types/database.types"; export * from "./types/events.types"; export * from "./types/kanban.types"; export * from "./types/removeNull"; +export * from "./types/stripe.types"; export * from "./types/tablos.types"; // Export utils export * from "./utils/helpers"; diff --git a/packages/shared/src/types/database.types.ts b/packages/shared/src/types/database.types.ts index cd9f8a1..45703d0 100644 --- a/packages/shared/src/types/database.types.ts +++ b/packages/shared/src/types/database.types.ts @@ -349,6 +349,7 @@ export type Database = { last_name: string | null; last_signed_in: string | null; name: string | null; + plan: Database["public"]["Enums"]["subscription_plan"] | null; short_user_id: string; }; Insert: { @@ -360,6 +361,7 @@ export type Database = { last_name?: string | null; last_signed_in?: string | null; name?: string | null; + plan?: Database["public"]["Enums"]["subscription_plan"] | null; short_user_id: string; }; Update: { @@ -371,6 +373,7 @@ export type Database = { last_name?: string | null; last_signed_in?: string | null; name?: string | null; + plan?: Database["public"]["Enums"]["subscription_plan"] | null; short_user_id?: string; }; Relationships: []; @@ -622,9 +625,100 @@ export type Database = { }; Functions: { generate_random_string: { Args: { length?: number }; Returns: string }; + get_my_active_subscription: { + Args: never; + Returns: { + billing_interval: string; + cancel_at_period_end: boolean; + currency: string; + current_period_end: string; + current_period_start: string; + first_name: string; + last_name: string; + plan: Database["public"]["Enums"]["subscription_plan"]; + product_name: string; + status: string; + subscription_id: string; + unit_amount: number; + user_email: string; + user_id: string; + }[]; + }; + get_stripe_prices: { + Args: never; + Returns: { + active: boolean; + created: number; + currency: string; + id: string; + metadata: Json; + product: string; + recurring: Json; + unit_amount: number; + }[]; + }; + get_stripe_products: { + Args: never; + Returns: { + active: boolean; + created: number; + description: string; + id: string; + metadata: Json; + name: string; + }[]; + }; + get_user_stripe_customer: { + Args: never; + Returns: { + created: number; + email: string; + id: string; + metadata: Json; + user_id: string; + }[]; + }; + get_user_stripe_customer_id: { + Args: { user_uuid: string }; + Returns: string; + }; + get_user_stripe_subscriptions: { + Args: never; + Returns: { + cancel_at_period_end: boolean; + canceled_at: number; + created: number; + current_period_end: number; + current_period_start: number; + customer: string; + id: string; + metadata: Json; + price_id: string; + quantity: number; + status: string; + trial_end: Json; + trial_start: Json; + user_id: string; + }[]; + }; + get_user_subscription_status: { + Args: { user_uuid: string }; + Returns: { + cancel_at_period_end: boolean; + current_period_end: number; + current_period_start: number; + plan: Database["public"]["Enums"]["subscription_plan"]; + price_id: string; + product_name: string; + status: string; + subscription_id: string; + }[]; + }; + is_paying_user: { Args: { user_uuid: string }; Returns: boolean }; }; Enums: { devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired"; + subscription_plan: "none" | "trial" | "standard"; }; CompositeTypes: { time_range: { @@ -754,6 +848,7 @@ export const Constants = { public: { Enums: { devis_status: ["draft", "sent", "accepted", "rejected", "expired"], + subscription_plan: ["none", "trial", "standard"], }, }, } as const; diff --git a/packages/shared/src/types/stripe.types.ts b/packages/shared/src/types/stripe.types.ts new file mode 100644 index 0000000..4da4039 --- /dev/null +++ b/packages/shared/src/types/stripe.types.ts @@ -0,0 +1,186 @@ +// ============================================================================ +// Stripe Types for Supabase Integration +// ============================================================================ + +// Types matching what the RPC functions return (Stripe API objects) +export interface StripeApiSubscription { + id: string; + customer: string; + user_id: string; + status: string; + items: unknown; + cancel_at_period_end: boolean; + current_period_start: number; + current_period_end: number; + created: number; + canceled_at: number | null; + trial_start: unknown; + trial_end: unknown; + metadata: Record; +} + +export interface StripeApiProduct { + id: string; + name: string; + description: string | null; + active: boolean; + created: number; + metadata: Record; +} + +export interface StripeApiPrice { + id: string; + product: string; + active: boolean; + currency: string; + unit_amount: number; + recurring: unknown; + created: number; + metadata: Record; +} + +export interface StripeCustomer { + id: string; + user_id: string; + stripe_customer_id: string; + email: string | null; + created_at: string; + updated_at: string; +} + +export interface StripeSubscription { + id: string; + user_id: string; + stripe_customer_id: string; + status: SubscriptionStatus; + price_id: string | null; + quantity: number | null; + cancel_at_period_end: boolean; + current_period_start: string | null; + current_period_end: string | null; + created_at: string; + updated_at: string; + canceled_at: string | null; + trial_end: string | null; + trial_start: string | null; +} + +export interface StripeProduct { + id: string; + active: boolean; + name: string; + description: string | null; + image: string | null; + metadata: Record | null; + created_at: string; + updated_at: string; +} + +export interface StripePrice { + id: string; + product_id: string; + active: boolean; + currency: string; + unit_amount: number; + interval: BillingInterval | null; + interval_count: number | null; + trial_period_days: number | null; + metadata: Record | null; + created_at: string; + updated_at: string; +} + +export type SubscriptionStatus = + | "active" + | "trialing" + | "past_due" + | "canceled" + | "unpaid" + | "incomplete" + | "incomplete_expired"; + +export type BillingInterval = "day" | "week" | "month" | "year"; + +export interface UserSubscriptionStatus { + subscription_id: string; + status: SubscriptionStatus; + current_period_end: string; + cancel_at_period_end: boolean; + price_id: string; + product_name: string; +} + +export interface PriceWithProduct extends StripePrice { + product: StripeProduct; +} + +export interface SubscriptionWithDetails extends StripeSubscription { + price: StripePrice; + product: StripeProduct; +} + +// Helper type guards +export function isActiveSubscription(status: SubscriptionStatus): boolean { + return status === "active" || status === "trialing"; +} + +export function isPastDue(status: SubscriptionStatus): boolean { + return status === "past_due"; +} + +export function isCanceled(status: SubscriptionStatus): boolean { + return status === "canceled"; +} + +// Format currency helper +export function formatPrice(amount: number, currency: string = "eur"): string { + return new Intl.NumberFormat("fr-FR", { + style: "currency", + currency: currency.toUpperCase(), + minimumFractionDigits: 0, + }).format(amount / 100); +} + +// Format interval helper +export function formatInterval(interval: BillingInterval | null, count: number = 1): string { + if (!interval) return ""; + + const intervals: Record = { + day: count === 1 ? "jour" : "jours", + week: count === 1 ? "semaine" : "semaines", + month: count === 1 ? "mois" : "mois", + year: count === 1 ? "an" : "ans", + }; + + return count > 1 ? `${count} ${intervals[interval]}` : intervals[interval]; +} + +// Get subscription status display text +export function getSubscriptionStatusText(status: SubscriptionStatus): string { + const statusTexts: Record = { + active: "Actif", + trialing: "Période d'essai", + past_due: "Paiement en retard", + canceled: "Annulé", + unpaid: "Impayé", + incomplete: "Incomplet", + incomplete_expired: "Expiré", + }; + + return statusTexts[status] || status; +} + +// Get subscription status color for UI +export function getSubscriptionStatusColor(status: SubscriptionStatus): string { + const colors: Record = { + active: "green", + trialing: "blue", + past_due: "orange", + canceled: "gray", + unpaid: "red", + incomplete: "yellow", + incomplete_expired: "red", + }; + + return colors[status] || "gray"; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b3c026..e366367 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -169,6 +169,9 @@ importers: '@react-stately/calendar': specifier: ^3.7.1 version: 3.9.0(react@19.0.0) + '@stripe/stripe-js': + specifier: ^8.2.0 + version: 8.2.0 '@supabase/supabase-js': specifier: ^2.49.3 version: 2.76.1 @@ -2872,6 +2875,10 @@ packages: resolution: {integrity: sha512-r6Qp0HylAZhHNWHxU1nGfRI2Dtkbs1iqLCnOp1bvKhv8yj0/sEUigN0dk0LGPbE4I7zDO3tppyd7PaTPBvvJkg==} engines: {node: '>=12'} + '@stripe/stripe-js@8.2.0': + resolution: {integrity: sha512-CSfD8HO5lKCEklhkV/WjusWqiU4j8JQl7X69CfslESmkUQ+E9/clmzuUbYnEnvNaFQRbYvryfkht/SpirGb2iA==} + engines: {node: '>=12.16'} + '@supabase/auth-js@2.76.1': resolution: {integrity: sha512-bxmcgPuyjTUBg7+jAohJ15TDh3ph4hXcv7QkRsQgnIpszurD5LYaJPzX638ETQ8zDL4fvHZRHfGrcmHV8C91jA==} @@ -9528,6 +9535,8 @@ snapshots: lodash.deburr: 4.1.0 optional: true + '@stripe/stripe-js@8.2.0': {} + '@supabase/auth-js@2.76.1': dependencies: '@supabase/node-fetch': 2.6.15 diff --git a/xtablo-expo/lib/database.types.ts b/xtablo-expo/lib/database.types.ts index 56ea694..2071436 100644 --- a/xtablo-expo/lib/database.types.ts +++ b/xtablo-expo/lib/database.types.ts @@ -355,6 +355,7 @@ export type Database = { last_name: string | null last_signed_in: string | null name: string | null + plan: Database["public"]["Enums"]["subscription_plan"] | null short_user_id: string } Insert: { @@ -366,6 +367,7 @@ export type Database = { last_name?: string | null last_signed_in?: string | null name?: string | null + plan?: Database["public"]["Enums"]["subscription_plan"] | null short_user_id: string } Update: { @@ -377,6 +379,7 @@ export type Database = { last_name?: string | null last_signed_in?: string | null name?: string | null + plan?: Database["public"]["Enums"]["subscription_plan"] | null short_user_id?: string } Relationships: [] @@ -628,9 +631,100 @@ export type Database = { } Functions: { generate_random_string: { Args: { length?: number }; Returns: string } + get_my_active_subscription: { + Args: never + Returns: { + billing_interval: string + cancel_at_period_end: boolean + currency: string + current_period_end: string + current_period_start: string + first_name: string + last_name: string + plan: Database["public"]["Enums"]["subscription_plan"] + product_name: string + status: string + subscription_id: string + unit_amount: number + user_email: string + user_id: string + }[] + } + get_stripe_prices: { + Args: never + Returns: { + active: boolean + created: number + currency: string + id: string + metadata: Json + product: string + recurring: Json + unit_amount: number + }[] + } + get_stripe_products: { + Args: never + Returns: { + active: boolean + created: number + description: string + id: string + metadata: Json + name: string + }[] + } + get_user_stripe_customer: { + Args: never + Returns: { + created: number + email: string + id: string + metadata: Json + user_id: string + }[] + } + get_user_stripe_customer_id: { + Args: { user_uuid: string } + Returns: string + } + get_user_stripe_subscriptions: { + Args: never + Returns: { + cancel_at_period_end: boolean + canceled_at: number + created: number + current_period_end: number + current_period_start: number + customer: string + id: string + metadata: Json + price_id: string + quantity: number + status: string + trial_end: Json + trial_start: Json + user_id: string + }[] + } + get_user_subscription_status: { + Args: { user_uuid: string } + Returns: { + cancel_at_period_end: boolean + current_period_end: number + current_period_start: number + plan: Database["public"]["Enums"]["subscription_plan"] + price_id: string + product_name: string + status: string + subscription_id: string + }[] + } + is_paying_user: { Args: { user_uuid: string }; Returns: boolean } } Enums: { devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired" + subscription_plan: "none" | "trial" | "standard" } CompositeTypes: { time_range: { @@ -762,6 +856,7 @@ export const Constants = { public: { Enums: { devis_status: ["draft", "sent", "accepted", "rejected", "expired"], + subscription_plan: ["none", "trial", "standard"], }, }, } as const