Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions app/STRIPE_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Stripe Subscription Setup Guide

This guide explains how to set up Stripe subscriptions for the Workouts application.

## Prerequisites

1. A Stripe account (can be a test account for development)
2. A Supabase project with the database migrations applied

## Environment Variables

Add the following environment variables to your `.env` file in the app directory:

```bash
# Stripe API keys
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key
STRIPE_SECRET_KEY=sk_test_your_secret_key
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret

# Supabase Service Role Key (for webhook operations)
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
SUPABASE_URL=your_supabase_url
```

> Note: For backward compatibility, the application also supports the legacy `VITE_` prefixed versions of these variables, but the non-prefixed versions are recommended.

## Stripe Product and Price Setup

1. Log in to your Stripe Dashboard: https://dashboard.stripe.com/
2. Go to Product Catalogue > Create Product
3. Create the following products and prices:

### Monthly Subscription
- Name: "Workouts Pro - Monthly"
- Description: "Full access to all workout features with cloud sync"
- Price:
- Currency: USD
- Amount: $8.99
- Recurring: Monthly
- Save the price ID (starts with "price_") and update in `pricing-plans.ts`

### Annual Subscription
- Name: "Workouts Pro - Annual"
- Description: "Full access with 2 months free"
- Price:
- Currency: USD
- Amount: $89.99
- Recurring: Annual
- Save the price ID (starts with "price_") and update in `pricing-plans.ts`

## Webhook Setup

1. In the Stripe Dashboard, go to Developers > Webhooks
2. Add an endpoint with your application's URL:
- Local development: Use Stripe CLI or a tunneling service like ngrok
- Production: https://your-domain.com/api/subscriptions/webhook
3. Add these events to listen for:
- customer.subscription.created
- customer.subscription.updated
- customer.subscription.deleted
- checkout.session.completed
4. Copy the Webhook Secret and add it to your `.env` file as `STRIPE_WEBHOOK_SECRET`

## Update Price IDs in the Application

Open `/src/lib/subscription/pricing-plans.ts` and update the Stripe product and price IDs with your actual values from Stripe:

```typescript
export const pricingPlans: PricingPlan[] = [
// ... free plan ...
{
id: 'monthly',
// ... other properties ...
stripe_product_id: 'prod_your_monthly_product_id',
stripe_price_id: 'price_your_monthly_price_id',
},
{
id: 'yearly',
// ... other properties ...
stripe_product_id: 'prod_your_yearly_product_id',
stripe_price_id: 'price_your_yearly_price_id',
}
];
```

## Testing Subscriptions

1. Use Stripe's test credit card numbers for testing:
- Successful payment: 4242 4242 4242 4242
- Failed payment: 4000 0000 0000 0002
2. Set an expiration date in the future, any CVC, and any billing address

### Listening to Webhook Events

- Ensure your server is running and can receive webhook events
- Use the Stripe CLI to forward events to your local server:
```bash
stripe listen --forward-to localhost:5173/api/subscriptions/webhook
```
- Copy the webhook signing secret from the CLI output and add it to your `.env` file as `STRIPE_WEBHOOK_SECRET`
- Test the webhook by triggering events from the Stripe Dashboard or using the CLI:
```bash
stripe trigger customer.subscription.created
```
- Check your server logs to confirm that the webhook was received and processed correctly
- Verify that the subscription status is updated in your Supabase database
- Check the Stripe Dashboard for the event logs to see if the webhook was successful
- Ensure that the subscription status is updated in your Supabase database


## Going Live

Before going live:

1. Switch your Stripe keys from test to production
2. Update your webhook endpoint to your production URL
3. Test the full subscription flow in your production environment

## Troubleshooting

### Webhook Issues
- Check webhook logs in Stripe Dashboard
- Ensure your webhook secret is correctly configured
- Verify that your server can receive POST requests at the webhook endpoint

### Subscription Issues
- Validate that the product and price IDs are correct
- Check the Stripe dashboard for subscription status
- Look for error logs in your application server
3 changes: 2 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.49.4",
"@vitest/coverage-v8": "3.0.8",
"dexie": "^4.0.11"
"dexie": "^4.0.11",
"stripe": "^18.1.0"
},
"devDependencies": {
"@eslint/compat": "^1.2.8",
Expand Down
26 changes: 26 additions & 0 deletions app/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion app/src/app.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "$lib/database/supabase-types";

declare global {
namespace App {
// interface Error {}
// interface Locals {}
interface Locals {
session: {
user: {
id: string;
email: string;
};
} | null;
getSupabaseServer: () => SupabaseClient<Database>;
}
// interface PageData {}
// interface PageState {}
// interface Platform {}
Expand Down
34 changes: 34 additions & 0 deletions app/src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// src/hooks.server.ts
import { createSupabaseServerClient } from "$lib/supabase";
import { redirect, type Handle, type RequestEvent } from "@sveltejs/kit";

async function getSession(event: RequestEvent) {
const supabaseServer = createSupabaseServerClient(event);
const {
data: { session },
error,
} = await supabaseServer.auth.getSession();
if (error) {
console.error("Error getting session:", error);
return null;
}
return session;
}

export const handle: Handle = async ({ event, resolve }) => {
// Ignore Chrome DevTools special paths to prevent 404 errors in logs
if (event.url.pathname.startsWith("/.well-known/appspecific")) {
return new Response(null, { status: 200 });
}

// Set the session in locals for server routes to access
event.locals.session = await getSession(event);

// Make Supabase available to server-side load functions
event.locals.getSupabaseServer = () => createSupabaseServerClient(event);

// Continue resolving the request
const response = await resolve(event);

return response;
};
12 changes: 12 additions & 0 deletions app/src/lib/components/NavigationMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { page } from "$app/state";
import { base } from "$app/paths";
import AuthDropdown from "$lib/components/AuthDropdown.svelte";
import { hasActiveSubscription } from "$lib/subscription/subscription-service";

// Track active route to highlight current page
let pathname = $derived(page.url.pathname);
Expand Down Expand Up @@ -72,6 +73,17 @@
Guidelines
</a>

<a
href="{base}/subscription"
class="subscription-link transition-colors {$hasActiveSubscription
? 'text-success hover:text-success-content'
: 'hover:text-primary'}
{pathname === base + '/subscription' ? 'font-bold' : ''}"
id="nav-subscription"
>
{$hasActiveSubscription ? "★ Pro" : "Upgrade"}
</a>

<!-- Add Authentication Dropdown -->
<div class="divider divider-horizontal mx-2"></div>
<AuthDropdown />
Expand Down
52 changes: 52 additions & 0 deletions app/src/lib/components/SubscriptionGuard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { user } from "$lib/supabase/client";
import { hasActiveSubscription } from "$lib/subscription/subscription-service";

export let showLoginRedirect = true;
export let redirectURL = "/subscription";

function handleNavigateToSubscription() {
goto(redirectURL);
}

function handleNavigateToLogin() {
goto(`/login?redirect=${redirectURL}`);
}
</script>

{#if $hasActiveSubscription}
<slot />
{:else}
<div class="subscription-guard-message card bg-base-100">
<div class="card-body items-center text-center">
<h2 class="card-title">Subscription Required</h2>
<p>
This feature requires an active subscription to unlock full
functionality.
</p>
{#if $user}
<div class="card-actions justify-end">
<button
class="btn btn-primary"
on:click={handleNavigateToSubscription}
>
View Subscription Plans
</button>
</div>
{:else if showLoginRedirect}
<div class="card-actions flex-col gap-2 sm:flex-row">
<button class="btn btn-primary" on:click={handleNavigateToLogin}>
Login
</button>
<button
class="btn btn-outline"
on:click={handleNavigateToSubscription}
>
View Subscription Plans
</button>
</div>
{/if}
</div>
</div>
{/if}
Loading
Loading