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

refactor Stripe webhook #200

Merged
merged 30 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7c06409
rename TierIds to PaymentPlanIds
vincanger Jun 28, 2024
5b736ab
refactor webhook and util functions
vincanger Jul 1, 2024
41f8884
pass userDelegate to function
vincanger Jul 2, 2024
cf079ac
Merge branch 'main' into refactor-subscription-logic
vincanger Jul 2, 2024
2ed6d6b
Update dbSeeds.ts
vincanger Jul 2, 2024
1f9c766
Merge branch 'main' into refactor-subscription-logic
vincanger Jul 2, 2024
f8c65b3
update app diff
vincanger Jul 2, 2024
b63924b
Update template/app/src/server/stripe/stripeClient.ts
vincanger Jul 3, 2024
eed60a3
extract event handlers and more
vincanger Jul 3, 2024
3c17bdb
Merge branch 'refactor-subscription-logic' of https://github.com/wasp…
vincanger Jul 3, 2024
4a5a69a
Update AccountPage.tsx
vincanger Jul 4, 2024
77f0517
address filips pro effective typescripting and stuff
vincanger Jul 4, 2024
373cb5e
Martin's attempt at consolidating types.
Martinsos Jul 4, 2024
de108da
fix
Martinsos Jul 4, 2024
b013f21
fix webhook events and validation
vincanger Jul 5, 2024
1b8dae1
small changes
vincanger Jul 5, 2024
3d40416
put stripe event handlers back for marty merge
vincanger Jul 6, 2024
cbc9d66
Merge branch 'refactor-subscription-logic-martin-attempt' into refact…
vincanger Jul 6, 2024
73089dc
merge consilidated types from martin
vincanger Jul 6, 2024
744c7db
move some types around
vincanger Jul 6, 2024
62e918b
add docs for stripe api version
vincanger Jul 6, 2024
71242c0
Update AccountPage.tsx
vincanger Jul 8, 2024
d88ad0f
Update stripe.ts
vincanger Jul 8, 2024
d89a35f
update SubscriptionStatus type
vincanger Jul 8, 2024
d62d186
Update actions.ts
vincanger Jul 8, 2024
6829173
add assertUnreachable util
vincanger Jul 8, 2024
4af43d4
more small changes
vincanger Jul 9, 2024
d5ae7c4
Update deploying.md
vincanger Jul 9, 2024
3f9836f
update accountPage and docs
vincanger Jul 10, 2024
b2174db
update app_diff
vincanger Jul 10, 2024
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
2 changes: 1 addition & 1 deletion opensaas-sh/app_diff/main.wasp.diff
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,5 @@
+ // the admin dashboard but won't be able to see the other users' data, only mock user data.
+ isMockUser Boolean @default(false)

stripeId String?
stripeId String? @unique
checkoutSessionId String?
8 changes: 5 additions & 3 deletions opensaas-sh/app_diff/src/server/scripts/dbSeeds.ts.diff
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
--- template/app/src/server/scripts/dbSeeds.ts
+++ opensaas-sh/app/src/server/scripts/dbSeeds.ts
@@ -43,5 +43,6 @@
@@ -43,5 +43,8 @@
datePaid: hasUserPaidOnStripe ? faker.date.between({ from: createdAt, to: lastActiveTimestamp }) : null,
checkoutSessionId: hasUserPaidOnStripe ? `cs_test_${faker.string.uuid()}` : null,
subscriptionTier: subscriptionStatus ? faker.helpers.arrayElement([TierIds.HOBBY, TierIds.PRO]) : null,
+ isMockUser: true,
subscriptionTier: subscriptionStatus ? faker.helpers.arrayElement([PaymentPlanIds.HOBBY, PaymentPlanIds.PRO]) : null,
+ // For the demo app, we want to default isMockUser to true so that our admin dash only shows mock users
+ // and not real users signing up to test the app
+ isMockUser: true
};
}
Binary file added opensaas-sh/blog/public/stripe/npm-version.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 41 additions & 1 deletion opensaas-sh/blog/src/content/docs/guides/deploying.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,48 @@ After deploying your server, you need to add the correct redirect URIs to the cr

### Setting up your Stripe Webhook

Now you need to set up your stripe webhook for production use.
Now you need to set up your stripe webhook for production use. Below are some important steps and considerations you should take as you prepare to deploy your app to production.

#### Stripe API Versions

When you create your Stripe account, Stripe will automatically assign you to their latest API version at that time. This API version is important because it determines the structure of the responses Stripe sends to your webhook.
Martinsos marked this conversation as resolved.
Show resolved Hide resolved

Because this template was built with a specific version of the Stripe API in mind, it could be that your Stripe account is set to a different API version.

:::note
```ts title="stripeClient.ts"
export const stripe = new Stripe(process.env.STRIPE_KEY!, {
apiVersion: 'YYYY-MM-DD', // e.g. 2023-08-16
});
```
Even if you specify a specific API version in your Stripe client, Stripe will still send some of the responses, like those made after a user completes a payment on checkout, using their latest version of the API. It's only when you send a request from your server (with a specified API version in your client) that Stripe sends a response back matching this version.
Martinsos marked this conversation as resolved.
Show resolved Hide resolved

This is why it's important to make sure your Stripe client version also matches the API version in your Stripe account, and to thoroughly test any changes you make to your Stripe client before deploying to production.
:::

To make sure your app is consistent with your Stripe account, here are some steps you can follow:

1. You can find your `default` API version in the Stripe dashboard under the [Developers](https://dashboard.stripe.com/developers) section.
2. Check that the API version in your `stripe/stripeClient.ts` file matches the default API version in your dashboard:
```ts title="stripeClient.ts" {2}
export const stripe = new Stripe(process.env.STRIPE_KEY!, {
apiVersion: 'YYYY-MM-DD', // e.g. 2023-08-16
});
```
3. If they don't match, you can upgrade/downgrade your Stripe NPM package to match the API version in your dashboard:
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
- If your default version is also the latest version of the API, you can simply upgrade to most current version of the Stripe NPM package.
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
- If your default version is not the latest version, and you don't want to [upgrade to the latest version](https://docs.stripe.com/upgrades#how-can-i-upgrade-my-api), because e.g. you have other projects that depend on the current version, you can find and install the Stripe NPM package version that matches your default API version by following these steps:
- Find and note the date of your default API version in the [developer dashboard](https://dashboard.stripe.com/developers).
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
- Go to the [Stripe NPM package](https://www.npmjs.com/package/stripe) page and hover over `Published` date column until you find the package release that matches your version. For example, here we find the NPM version that matches the default API version of `2023-08-16` in our dashboard, which is `13.x.x`.
![stripe-npm-versions](/stripe/npm-version.png)
- Install the correct version of the Stripe NPM package by running, :
```sh
npm install [email protected] # e.g. npm install [email protected]
```
4. **Test your app thoroughly** to make sure that the changes you made to your Stripe client are working as expected before deploying to production.


#### Creating Your Production Webhook
1. go to [https://dashboard.stripe.com/webhooks](https://dashboard.stripe.com/webhooks)
2. click on `+ add endpoint`
3. enter your endpoint url, which will be the url of your deployed server + `/stripe-webhook`, e.g. `https://open-saas-wasp-sh-server.fly.dev/stripe-webhook`
Expand Down
6 changes: 3 additions & 3 deletions template/app/main.wasp
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@ entity User {=psl
lastActiveTimestamp DateTime @default(now())
isAdmin Boolean @default(false)

stripeId String?
stripeId String? @unique
checkoutSessionId String?
subscriptionStatus String? // 'active', 'canceled', 'past_due', 'deleted', null
subscriptionTier String? // 'hobby-tier', 'pro-tier', null
subscriptionStatus String? // 'active', 'canceled', 'past_due', 'deleted'
subscriptionPlan String? // 'hobby', 'pro'
sendEmail Boolean @default(false)
datePaid DateTime?
credits Int @default(3)
Expand Down
2 changes: 1 addition & 1 deletion template/app/src/client/admin/components/CheckboxOne.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { cn } from '../../../shared/utils';
import { cn } from '../../cn';

const CheckboxOne = () => {
const [isChecked, setIsChecked] = useState<boolean>(false);
Expand Down
2 changes: 1 addition & 1 deletion template/app/src/client/admin/components/CheckboxTwo.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { cn } from '../../../shared/utils';
import { cn } from '../../cn';

const CheckboxTwo = () => {
const [enabled, setEnabled] = useState<boolean>(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cn } from '../../../shared/utils';
import { cn } from '../../cn';
import useColorMode from '../../hooks/useColorMode';

const DarkModeSwitcher = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { cn } from '../../../shared/utils';
import { cn } from '../../cn';

const DropdownDefault = () => {
const [dropdownOpen, setDropdownOpen] = useState(false);
Expand Down
2 changes: 1 addition & 1 deletion template/app/src/client/admin/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type AuthUser } from 'wasp/auth/types';
import DarkModeSwitcher from './DarkModeSwitcher';
import MessageButton from './MessageButton';
import DropdownUser from '../../components/DropdownUser';
import { cn } from '../../../shared/utils';
import { cn } from '../../cn';

const Header = (props: {
sidebarOpen: string | boolean | undefined;
Expand Down
2 changes: 1 addition & 1 deletion template/app/src/client/admin/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import Logo from '../../static/logo.png';
import SidebarLinkGroup from './SidebarLinkGroup';
import { cn } from '../../../shared/utils';
import { cn } from '../../cn';

interface SidebarProps {
sidebarOpen: boolean;
Expand Down
2 changes: 1 addition & 1 deletion template/app/src/client/admin/components/SwitcherOne.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type User } from 'wasp/entities';
import { useState } from 'react';
import { cn } from '../../../shared/utils';
import { cn } from '../../cn';

const SwitcherOne = ({ user, updateUserById }: { user?: Partial<User>; updateUserById?: any }) => {
const [enabled, setEnabled] = useState<boolean>(user?.isAdmin || false);
Expand Down
2 changes: 1 addition & 1 deletion template/app/src/client/admin/components/SwitcherTwo.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { cn } from '../../../shared/utils';
import { cn } from '../../cn';

const SwitcherTwo = () => {
const [enabled, setEnabled] = useState(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cn } from '../../../shared/utils';
import { cn } from '../../cn';
import { UpArrow, DownArrow } from '../images/icon/icons-arrows';

type PageViewsStats = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { UpArrow, DownArrow } from '../images/icon/icons-arrows';
import type { DailyStatsProps } from '../common/types';
import { cn } from '../../../shared/utils';
import { cn } from '../../cn';

const TotalPayingUsersCard = ({ dailyStats, isLoading }: DailyStatsProps) => {
const isDeltaPositive = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useEffect } from 'react';
import { useMemo } from 'react';
import { UpArrow, DownArrow } from '../images/icon/icons-arrows';
import type { DailyStatsProps } from '../common/types';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { UpArrow } from '../images/icon/icons-arrows';
import type { DailyStatsProps } from '../common/types';
import { cn } from '../../../shared/utils';
import { cn } from '../../cn';

const TotalSignupsCard = ({ dailyStats, isLoading }: DailyStatsProps) => {
const isDeltaPositive = useMemo(() => {
Expand Down
10 changes: 5 additions & 5 deletions template/app/src/client/admin/components/UsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { useState, useEffect } from 'react';
import SwitcherOne from './SwitcherOne';
import Loader from '../common/Loader';
import DropdownEditDelete from './DropdownEditDelete';
import { type SubscriptionStatusOptions } from '../../../shared/types';
import { type SubscriptionStatus } from '../../../payment/plans';

const UsersTable = () => {
const [skip, setskip] = useState(0);
const [page, setPage] = useState(1);
const [email, setEmail] = useState<string | undefined>(undefined);
const [isAdminFilter, setIsAdminFilter] = useState<boolean | undefined>(undefined);
const [statusOptions, setStatusOptions] = useState<SubscriptionStatusOptions[]>([]);
const [statusOptions, setStatusOptions] = useState<SubscriptionStatus[]>([]);
const { data, isLoading, error } = useQuery(getPaginatedUsers, {
skip,
emailContains: email,
Expand Down Expand Up @@ -93,10 +93,10 @@ const UsersTable = () => {
onChange={(e) => {
const targetValue = e.target.value === '' ? null : e.target.value;
setStatusOptions((prevValue) => {
if (prevValue?.includes(targetValue as SubscriptionStatusOptions)) {
if (prevValue?.includes(targetValue as SubscriptionStatus)) {
return prevValue?.filter((val) => val !== targetValue);
} else if (!!prevValue) {
return [...prevValue, targetValue as SubscriptionStatusOptions];
return [...prevValue, targetValue as SubscriptionStatus];
} else {
return prevValue;
}
Expand All @@ -108,7 +108,7 @@ const UsersTable = () => {
>
<option value=''>Select filters</option>
{['past_due', 'canceled', 'active', 'deleted', null].map((status) => {
if (!statusOptions.includes(status as SubscriptionStatusOptions)) {
if (!statusOptions.includes(status as SubscriptionStatus)) {
return <option value={status || ''}>{status ? status : 'has not subscribed'}</option>;
}
})}
Expand Down
93 changes: 68 additions & 25 deletions template/app/src/client/app/AccountPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { User } from 'wasp/entities';
import type { SubscriptionStatus } from '../../payment/plans';
import { PaymentPlanId, parsePaymentPlanId } from '../../payment/plans';
import { Link } from 'wasp/client/router';
import { type User } from 'wasp/entities';
import { logout } from 'wasp/client/auth';
import { TierIds } from '../../shared/constants';
import { z } from 'zod';

export default function AccountPage({ user }: { user: User }) {
Expand All @@ -27,27 +28,7 @@ export default function AccountPage({ user }: { user: User }) {
)}
<div className='py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6'>
<dt className='text-sm font-medium text-gray-500 dark:text-white'>Your Plan</dt>
{!!user.subscriptionStatus ? (
<>
{user.subscriptionStatus !== 'past_due' ? (
<dd className='mt-1 text-sm text-gray-900 dark:text-gray-400 sm:col-span-1 sm:mt-0'>
{user.subscriptionTier === TierIds.HOBBY ? 'Hobby' : 'Pro'} Plan
</dd>
) : (
<dd className='mt-1 text-sm text-gray-900 dark:text-gray-400 sm:col-span-1 sm:mt-0'>
Your Account is Past Due! Please Update your Payment Information
</dd>
)}
<CustomerPortalButton />
</>
) : (
<>
<dd className='mt-1 text-sm text-gray-900 dark:text-gray-400 sm:col-span-1 sm:mt-0'>
Credits remaining: {user.credits}
</dd>
<BuyMoreButton />
</>
)}
<UserCurrentPaymentPlan {...user} />
</div>
<div className='py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6'>
<dt className='text-sm font-medium text-gray-500 dark:text-white'>About</dt>
Expand All @@ -70,6 +51,65 @@ export default function AccountPage({ user }: { user: User }) {
);
}

function UserCurrentPaymentPlan({ subscriptionPlan, subscriptionStatus, datePaid, credits }: Pick<User, 'subscriptionPlan' | 'subscriptionStatus' | 'datePaid' | 'credits'>) {
vincanger marked this conversation as resolved.
Show resolved Hide resolved
if (subscriptionStatus && subscriptionPlan && datePaid) {
return (
<>
<dd className='mt-1 text-sm text-gray-900 dark:text-gray-400 sm:col-span-1 sm:mt-0'>
{getUserSubscriptionPlanMessage({ subscriptionPlan, subscriptionStatus, datePaid })}
</dd>
{subscriptionStatus as SubscriptionStatus !== 'deleted' ? <CustomerPortalButton /> : <BuyMoreButton />}
</>
);
}

return (
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
<>
<dd className='mt-1 text-sm text-gray-900 dark:text-gray-400 sm:col-span-1 sm:mt-0'>
Credits remaining: {credits}
</dd>
<BuyMoreButton />
</>
);
}

function getUserSubscriptionPlanMessage({
subscriptionPlan,
subscriptionStatus,
datePaid,
}: {
subscriptionPlan: string
subscriptionStatus: string
datePaid: Date;
}) {
const planName = prettyPaymentPlanName(parsePaymentPlanId(subscriptionPlan));
const endOfBillingPeriod = prettyPrintEndOfBillingPeriod(datePaid);

const statusToMessage: Record<SubscriptionStatus, string> = {
active: `${planName}`,
past_due: `Payment for your ${planName} plan is past due! Please update your subscription payment information.`,
cancel_at_period_end: `Your ${planName} plan subscription has been canceled, but remains active until the end of the current billing period${endOfBillingPeriod}`,
deleted: `Your previous subscription has been canceled and is no longer active.`,
};

return statusToMessage[subscriptionStatus as SubscriptionStatus];
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
}

function prettyPaymentPlanName (planId: PaymentPlanId): string {
const planToName: Record<PaymentPlanId, string> = {
[PaymentPlanId.Hobby]: 'Hobby',
[PaymentPlanId.Pro]: 'Pro',
[PaymentPlanId.Credits10]: '10 Credits'
};
return planToName[planId];
}

function prettyPrintEndOfBillingPeriod(date: Date) {
const oneMonthFromNow = new Date(date);
oneMonthFromNow.setMonth(oneMonthFromNow.getMonth() + 1);
return ': ' + oneMonthFromNow.toLocaleDateString();
}

function BuyMoreButton() {
return (
<div className='ml-4 flex-shrink-0 sm:col-span-1 sm:mt-0'>
Expand All @@ -87,13 +127,16 @@ function CustomerPortalButton() {
const customerPortalUrl = schema.parse(import.meta.env.REACT_APP_STRIPE_CUSTOMER_PORTAL);
window.open(customerPortalUrl, '_blank');
} catch (err) {
console.error(err)
console.error(err);
}
};

return (
<div className='ml-4 flex-shrink-0 sm:col-span-1 sm:mt-0'>
<button onClick={handleClick} className='font-medium text-sm text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300'>
<button
onClick={handleClick}
className='font-medium text-sm text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300'
>
Manage Subscription
</button>
</div>
Expand Down
7 changes: 3 additions & 4 deletions template/app/src/client/app/DemoAppPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import {
import { useState, useMemo } from 'react';
import { CgSpinner } from 'react-icons/cg';
import { TiDelete } from 'react-icons/ti';
import { type GeneratedSchedule } from '../../shared/types';
import { MainTask, Subtask } from '../../shared/types';
import { cn } from '../../shared/utils';
import type { GeneratedSchedule, MainTask, SubTask } from '../../gpt/schedule';
import { cn } from '../cn';

export default function DemoAppPage() {
return (
Expand Down Expand Up @@ -319,7 +318,7 @@ function TaskTable({ schedule }: { schedule: GeneratedSchedule }) {
);
}

function MainTaskTable({ mainTask, subtasks }: { mainTask: MainTask; subtasks: Subtask[] }) {
function MainTaskTable({ mainTask, subtasks }: { mainTask: MainTask; subtasks: SubTask[] }) {
return (
<>
<thead>
Expand Down
Loading
Loading