Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: deno task init:stripe and further documentation improvements #93

Merged
merged 9 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
76 changes: 25 additions & 51 deletions README.md

This comment was marked as resolved.

Original file line number Diff line number Diff line change
Expand Up @@ -71,68 +71,39 @@ This will automatically configure the database tables and their settings for us.
file as `SUPABASE_URL`, `SUPABASE_ANON_KEY`, and `SUPABASE_SERVICE_KEY`,
respectively.

### Payments

1. Create the “Premium tier” product in Stripe via the Stripe CLI:

```
stripe products create \
--name="Premium tier" \
--default-price-data.unit-amount=500 \
--default-price-data.currency=usd \
--default-price-data.recurring.interval=month \
--description="Unlimited todos"
```
### Payments and Subscriptions

2. The resulting [product object](https://stripe.com/docs/api/products) will be
printed in the terminal. Copy the `default_price` value as
`STRIPE_PREMIUM_PLAN_PRICE_ID` in [`constants.ts`](constants.ts).
1. Copy your Stripe secret key as `STRIPE_SECRET_KEY` into your `.env` file. We
recommend using the test key for your development environment.
2. Run `deno task init:stripe` and follow the instructions. This automatically
creates your "Premium tier" product and configures the Stripe customer
portal.

3. Next, head over to
[your Stripe dashboard settings](https://dashboard.stripe.com/test/apikeys)
to copy the `STRIPE_SECRET_KEY` into your `.env` file. We recommend using the
test key for your development environment.
> Note: go to [init/stripe.ts] if you'd like to learn more about how the
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@niklasmtj, regarding #93 (comment), I'd rather have one or the other. I've added this comment to point people in the right direction if they want to learn more. At some point, I'll include technical details as JSDoc comments in the implementation to give an even more precise idea of what's happening. Do you think this addresses your concern?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me 👍🏼

> `init:stripe` task works.

4. Configure Stripe's customer portal settings:
3. Listen locally to Stripe events:

5. Ensure your users can manage their subscription via Stripe's Customer portal
by going to
[your Customer Portal settings](https://dashboard.stripe.com/test/settings/billing/portal)
and toggling on:

- Payment methods > Allow customers to view and update payment methods.
- Cancellations > Cancel subscriptions
- Subscriptions > Customers can switch plans (and select your relevant
subscription tiers)

Hit save.

5. (Optional)
[Set up your branding on Stripe](https://dashboard.stripe.com/settings/branding),
as a user will be taken to Stripe's checkout page when they upgrade.
```
stripe listen --forward-to localhost:8000/api/subscription
```

#### Updating `customers` database via Stripe webhooks
4. Copy the webhook signing secret to [.env](.env) as `STRIPE_WEBHOOK_SECRET`.

Keep your `customers` database up to date with billing changes by
[registering a webhook endpoint in Stripe](https://stripe.com/docs/development/dashboard/register-webhook).
### Running the Server

To test locally, use the Stripe CLI:
Finally, start the server by running:

```
stripe listen --forward-to localhost:8000/api/subscription
deno task start
```

You'll receive an output that includes your webhook signing secret. Copy that
into your `.env` file as `STRIPE_WEBHOOK_SECRET`.

Start the server with `deno task start` and test the webhook with
`stripe trigger customer.subscription.created` or
`stripe trigger customer.subscription.deleted`.
Go to [http://localhost:8000](http://localhost:8000) to begin playing with your
new SaaS app.

#### Testing Payments

You can use [Stripe's test credit cards](https://stripe.com/docs/testing) to
make test payments while in Stripe's test mode.
> Note: You can use
> [Stripe's test credit cards](https://stripe.com/docs/testing) to make test
> payments while in Stripe's test mode.

### Global Constants

Expand All @@ -155,7 +126,7 @@ Once Docker and Supabase services are running, start the project with:
deno task start
```

Then, point your browser to `localhost:8000`.
Then, point your browser to `http://localhost:8000`.

## Deploying to Production

Expand All @@ -167,6 +138,9 @@ TODO

### Payments

[Set up your branding on Stripe](https://dashboard.stripe.com/settings/branding),
as a user will be taken to Stripe's checkout page when they upgrade.

Keep your `customers` database up to date with billing changes by
[registering a webhook endpoint in Stripe](https://stripe.com/docs/development/dashboard/register-webhook).

Expand Down
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"lock": false,
"tasks": {
"init:stripe": "deno run --allow-read --allow-env --allow-net init/stripe.ts ",
"start": "deno run -A --watch=static/,routes/ dev.ts",
"test": "deno test -A",
"check:license": "deno run --allow-read tools/check_license.ts"
Expand Down
4 changes: 1 addition & 3 deletions dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.

import dev from "$fresh/dev.ts";
import { load } from "std/dotenv/mod.ts";

load({ export: true });
import "std/dotenv/load.ts";

await dev(import.meta.url, "./main.ts");
74 changes: 74 additions & 0 deletions init/stripe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import type { Stripe } from "stripe";
import { SITE_DESCRIPTION } from "@/constants.ts";
import "std/dotenv/load.ts";
import { stripe } from "@/utils/stripe.ts";

async function createPremiumTierProduct(stripe: Stripe) {
/**
* These values provide a set of default values for the demo.
* However, these can be adjusted to fit your use case.
*/
return await stripe.products.create({
name: "Premium tier",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if the stuff in this object should be configurable? Does that make any sense?

Copy link
Contributor Author

@iuioiua iuioiua Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion. Though, we'll leave this as-is for now in the spirit of prioritising ease of setup over customizability. Either way, I've improved the comment.

description: "Unlimited todos",
default_price_data: {
unit_amount: 500,
currency: "usd",
recurring: {
interval: "month",
},
},
});
}

async function createDefaultPortalConfiguration(
stripe: Stripe,
product:
Stripe.BillingPortal.ConfigurationCreateParams.Features.SubscriptionUpdate.Product,
) {
return await stripe.billingPortal.configurations.create({
features: {
payment_method_update: {
enabled: true,
},
customer_update: {
allowed_updates: ["email", "name"],
enabled: true,
},
subscription_cancel: {
enabled: true,
mode: "immediately",
},
subscription_update: {
enabled: true,
default_allowed_updates: ["price"],
products: [product],
},
invoice_history: { enabled: true },
},
business_profile: {
headline: SITE_DESCRIPTION,
},
});
}

async function main() {
const product = await createPremiumTierProduct(stripe);

if (typeof product.default_price !== "string") return;

await createDefaultPortalConfiguration(stripe, {
prices: [product.default_price],
product: product.id,
});

console.log(
"Please copy and paste this value into the `STRIPE_PREMIUM_PLAN_PRICE_ID` constant in `constants.ts`: " +
product.default_price,
);
}

if (import.meta.main) {
await main();
}