RevenueCat Capacitor IAP; keep Stripe on web (#100) #102

Merged
westfarn merged 3 commits from feature/revenuecat-iap-100 into master 2026-08-04 03:43:10 -07:00
11 changed files with 27 additions and 27 deletions
Showing only changes of commit bf8f6ea1d2 - Show all commits
+2 -2
View File
@@ -4,10 +4,10 @@
| Runtime | Checkout / manage | Ledger |
|---------|-------------------|--------|
| Web (`!isNativePlatform`) | Stripe Checkout + Customer Portal via `/finance/...` | Stripe webhooks |
| Web (`!isNativePlatform`) | Stripe Checkout + Customer Portal via `/monetization/...` | Stripe webhooks |
| Native Capacitor | RevenueCat IAP (`@revenuecat/purchases-capacitor`) | RevenueCat webhooks → same invoice/payment tables |
FE displays whatever `/finance/invoices/` (and subscription) returns after refresh. Provider badge uses `provider` + `revenuecat_store`.
FE displays whatever `/monetization/invoices/` (and subscription) returns after refresh. Provider badge uses `provider` + `revenuecat_store`.
## App user id
@@ -157,16 +157,16 @@ const mockFinanceGets = ({
plans?: unknown[];
} = {}) => {
mockGet.mockImplementation((url: string) => {
if (url === '/finance/invoices/') {
if (url === '/monetization/invoices/') {
return Promise.resolve({ data: invoices });
}
if (url === '/finance/payments/') {
if (url === '/monetization/payments/') {
return Promise.resolve({ data: payments });
}
if (url === '/finance/subscription/') {
if (url === '/monetization/subscription/') {
return Promise.resolve({ data: subscription });
}
if (url === '/finance/plans/') {
if (url === '/monetization/plans/') {
return Promise.resolve({ data: plans });
}
return Promise.reject(new Error(`unexpected GET ${url}`));
@@ -246,7 +246,7 @@ describe('BillingSection', () => {
await user.click(screen.getByRole('button', { name: /Change plan/i }));
await waitFor(() => {
expect(mockPost).toHaveBeenCalledWith('/finance/portal/', {
expect(mockPost).toHaveBeenCalledWith('/monetization/portal/', {
return_url: 'http://localhost/account/',
});
});
@@ -321,7 +321,7 @@ describe('BillingSection', () => {
await waitFor(() => {
expect(mockPost).toHaveBeenCalledWith(
'/finance/checkout/',
'/monetization/checkout/',
expect.objectContaining({
success_url: expect.stringContaining('/billing/success'),
cancel_url: expect.stringContaining('/billing/cancel'),
@@ -378,7 +378,7 @@ describe('BillingSection', () => {
expect(mockPurchasePlan).toHaveBeenCalled();
});
expect(mockPost).not.toHaveBeenCalledWith(
'/finance/checkout/',
'/monetization/checkout/',
expect.anything()
);
});
@@ -292,10 +292,10 @@ const BillingSection = (): JSX.Element => {
try {
const [invoiceResponse, paymentResponse, subscriptionResponse, plansResponse] =
await Promise.all([
axiosInstance.get<FinanceInvoice[]>('/finance/invoices/'),
axiosInstance.get<FinancePayment[]>('/finance/payments/'),
axiosInstance.get<SubscriptionMe>('/finance/subscription/'),
axiosInstance.get<SubscriptionPlanInfo[]>('/finance/plans/'),
axiosInstance.get<FinanceInvoice[]>('/monetization/invoices/'),
axiosInstance.get<FinancePayment[]>('/monetization/payments/'),
axiosInstance.get<SubscriptionMe>('/monetization/subscription/'),
axiosInstance.get<SubscriptionPlanInfo[]>('/monetization/plans/'),
]);
setInvoices(Array.isArray(invoiceResponse.data) ? invoiceResponse.data : []);
setPayments(Array.isArray(paymentResponse.data) ? paymentResponse.data : []);
@@ -385,7 +385,7 @@ const BillingSection = (): JSX.Element => {
try {
const returnUrl = `${window.location.origin}/account/`;
const response = await axiosInstance.post<{ portal_url: string }>(
'/finance/portal/',
'/monetization/portal/',
{ return_url: returnUrl }
);
const portalUrl = response.data?.portal_url;
@@ -461,7 +461,7 @@ const BillingSection = (): JSX.Element => {
...(planSlug ? { plan_slug: planSlug } : {}),
});
const response = await axiosInstance.post<{ checkout_url: string }>(
'/finance/checkout/',
'/monetization/checkout/',
{
success_url,
cancel_url,
@@ -86,7 +86,7 @@ describe('Header2 (#81 subscription-aware Documents nav link)', () => {
renderHeader();
await waitFor(() => expect(mockGet).toHaveBeenCalledWith('/finance/subscription/'));
await waitFor(() => expect(mockGet).toHaveBeenCalledWith('/monetization/subscription/'));
expect(screen.queryByText('Documents')).not.toBeInTheDocument();
});
@@ -174,8 +174,8 @@ const UsageSummaryCard = (): JSX.Element => {
setLoading(true);
try {
const [subscriptionResponse, invoiceResponse] = await Promise.all([
axiosInstance.get<SubscriptionMe>('/finance/subscription/'),
axiosInstance.get<FinanceInvoice[]>('/finance/invoices/'),
axiosInstance.get<SubscriptionMe>('/monetization/subscription/'),
axiosInstance.get<FinanceInvoice[]>('/monetization/invoices/'),
]);
setSubscription(subscriptionResponse.data || null);
setHasPortalAccess(
@@ -231,7 +231,7 @@ const UsageSummaryCard = (): JSX.Element => {
try {
const returnUrl = `${window.location.origin}/account/`;
const response = await axiosInstance.post<{ portal_url: string }>(
'/finance/portal/',
'/monetization/portal/',
{ return_url: returnUrl }
);
if (response.data?.portal_url) {
+3 -3
View File
@@ -6,7 +6,7 @@ import { planAllowsRag, SubscriptionMe } from '../utils/finance';
/**
* Module-level cache so every component that calls useSubscription() during the
* same session shares one /finance/subscription/ request instead of each
* same session shares one /monetization/subscription/ request instead of each
* mounting its own (e.g. Header2 + DocumentStoragePage on the same page).
*/
let cachedSubscription: SubscriptionMe | null = null;
@@ -18,7 +18,7 @@ async function loadSubscription(): Promise<SubscriptionMe | null> {
}
if (!inFlightRequest) {
inFlightRequest = axiosInstance
.get<SubscriptionMe>('/finance/subscription/')
.get<SubscriptionMe>('/monetization/subscription/')
.then((response: AxiosResponse<SubscriptionMe>) => {
cachedSubscription = response.data || null;
return cachedSubscription;
@@ -44,7 +44,7 @@ export type UseSubscriptionResult = {
refresh: () => Promise<void>;
};
/** Fetches /finance/subscription/ once per session (shared across callers) and exposes plan-derived flags. */
/** Fetches /monetization/subscription/ once per session (shared across callers) and exposes plan-derived flags. */
export function useSubscription(): UseSubscriptionResult {
const { authenticated } = useContext(AuthContext);
const [subscription, setSubscription] = useState<SubscriptionMe | null>(cachedSubscription);
@@ -133,7 +133,7 @@ describe('AuthCallback', () => {
await waitFor(() => {
expect(mockPost).toHaveBeenCalledWith(
'/finance/checkout/',
'/monetization/checkout/',
expect.objectContaining({
success_url: expect.stringContaining('/billing/success'),
cancel_url: expect.stringContaining('/billing/cancel'),
@@ -181,7 +181,7 @@ const AuthCallback = (): JSX.Element => {
setStatusText('Starting checkout…');
const { success_url, cancel_url } = checkoutReturnUrls();
trackEvent(AnalyticsEvents.CHECKOUT_STARTED, { source: 'sso_signup' });
const checkoutResponse = await axiosInstance.post('/finance/checkout/', {
const checkoutResponse = await axiosInstance.post('/monetization/checkout/', {
success_url,
cancel_url,
});
@@ -60,7 +60,7 @@ const subscriptionResponse = (rag: boolean) => ({
const mockGetByUrl = (overrides: { rag: boolean }) => {
mockGet.mockImplementation((url: string) => {
if (url === '/finance/subscription/') {
if (url === '/monetization/subscription/') {
return Promise.resolve(subscriptionResponse(overrides.rag));
}
if (typeof url === 'string' && url.startsWith('/documents/')) {
@@ -139,7 +139,7 @@ describe('SignUp', () => {
});
await waitFor(() => {
expect(mockPost).toHaveBeenCalledWith('/finance/checkout/', {
expect(mockPost).toHaveBeenCalledWith('/monetization/checkout/', {
success_url: 'http://localhost/billing/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url: 'http://localhost/billing/cancel',
});
+1 -1
View File
@@ -271,7 +271,7 @@ const SignUp = (): JSX.Element => {
const { success_url, cancel_url } = checkoutReturnUrls();
trackEvent(AnalyticsEvents.CHECKOUT_STARTED, { source: 'signup' });
const checkoutResponse = await axiosInstance.post('/finance/checkout/', {
const checkoutResponse = await axiosInstance.post('/monetization/checkout/', {
success_url,
cancel_url,
});