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

Stripe Integration #886

Merged
merged 60 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
be99abb
Add Stripe integration and payments feature
ukrocks007 Dec 21, 2023
1c0c1d3
Add middleware route for Stripe webhook
ukrocks007 Dec 29, 2023
977ce59
Add Stripe integration and models
ukrocks007 Jan 2, 2024
5c9bbb2
Refactor product pricing and stripe integration
ukrocks007 Jan 2, 2024
47ae0a7
Fix billing scheme comparison in productPricing.tsx and recurring fie…
ukrocks007 Jan 2, 2024
feabcfd
Merge branch 'main' into stripe
ukrocks007 Jan 2, 2024
82cee41
Remove unused Stripe publishable key
ukrocks007 Jan 2, 2024
824694c
Update icon for payments tab
ukrocks007 Jan 2, 2024
c492070
Refactor Stripe configuration and environment variables
ukrocks007 Jan 2, 2024
8ba31bf
Remove unused import in payments/products.ts
ukrocks007 Jan 2, 2024
0ee3be4
Remove unused function getSubscriptionsWithItems()
ukrocks007 Jan 2, 2024
f82d55d
Refactor stripePrice and stripeProduct models
ukrocks007 Jan 2, 2024
0037ed1
Update stripe package version to 12.6.0
ukrocks007 Jan 2, 2024
84e7878
Add toast notifications and error handling
ukrocks007 Jan 2, 2024
3bd4fc0
Refactor error handling in payments.tsx
ukrocks007 Jan 2, 2024
7f8bbf8
Refactor code: Remove unused interfaces and update object destructuring
ukrocks007 Jan 2, 2024
a8b9b55
Update imports and handle undefined value in Stripe constructor
ukrocks007 Jan 2, 2024
50e8256
Merge branch 'main' into stripe
devkiran Jan 9, 2024
aaa7da6
Refactor API handlers for payments
devkiran Jan 9, 2024
2be2977
Code review changes
ukrocks007 Jan 9, 2024
5dfc30a
Merge branch 'main' into stripe
ukrocks007 Jan 9, 2024
42367ac
Merge branch 'main' into stripe
Jan 10, 2024
4424221
Remove unused dependencies
ukrocks007 Jan 10, 2024
ba9fad0
Add Stripe-related tables and columns
ukrocks007 Jan 10, 2024
d709964
Refactor payments feature in env.ts
ukrocks007 Jan 10, 2024
b2292b0
Refactor plan image rendering in ProductPricing component
ukrocks007 Jan 10, 2024
03e21db
Merge branch 'main' into stripe
Jan 15, 2024
553ee37
Merge branch 'main' into stripe
ukrocks007 Jan 15, 2024
d327dcf
Merge branch 'main' into stripe
ukrocks007 Jan 15, 2024
cf7f264
Refactor env.ts to simplify payments feature check
ukrocks007 Jan 15, 2024
0a3cee8
Update stripe.ts
ukrocks007 Jan 15, 2024
a2248b5
Update stripe.ts
ukrocks007 Jan 15, 2024
5d4a7a3
Add plan, price, and subscription models
ukrocks007 Jan 17, 2024
6785a47
Refactor code to improve performance and readability
ukrocks007 Jan 22, 2024
3eb2a8b
Merge branch 'main' into stripe
devkiran Jan 23, 2024
4cbfbd7
Refactor code to improve performance and readability
ukrocks007 Jan 23, 2024
ec7259c
update page
devkiran Jan 23, 2024
f99db6c
Make some UI changes to /billing
devkiran Jan 23, 2024
8e503f4
renamed Plan to Service and related fixes
ukrocks007 Jan 23, 2024
2ae1d24
Add amount and metadata fields to Price model
ukrocks007 Jan 23, 2024
bcc0b10
adjust for smaller screen
devkiran Jan 23, 2024
c0e0126
Make UI Changes
devkiran Jan 23, 2024
7bcdada
Merge branch 'main' into stripe
ukrocks007 Jan 24, 2024
0ad4628
Format
devkiran Jan 24, 2024
a2d7443
Merge branch 'stripe' of https://github.com/boxyhq/saas-starter-kit i…
devkiran Jan 25, 2024
bb27d69
Update dep
devkiran Jan 25, 2024
c337611
Merge branch 'main' into stripe
devkiran Jan 26, 2024
a1440e8
Update localization and add translation for billing components
devkiran Jan 26, 2024
42d1888
Format
devkiran Jan 26, 2024
ee4ffa6
Refactor syncStripe.js to use transaction for database operations
ukrocks007 Feb 9, 2024
b44710e
Add new tables and columns for billing and subscriptions
ukrocks007 Feb 9, 2024
e628b1a
Add support URL to env and update Help component
ukrocks007 Feb 9, 2024
f097b26
Merge branch 'main' into stripe
ukrocks007 Feb 9, 2024
ca0029d
Merge branch 'main' into stripe
deepakprabhakara Feb 10, 2024
7dddfb8
Add migration for Stripe integration and update schema.prisma
ukrocks007 Feb 12, 2024
3c2ff90
Merge branch 'main' into stripe
ukrocks007 Feb 13, 2024
e32799e
Remove unique constraint on Subscription_id_key and update Subscripti…
ukrocks007 Feb 13, 2024
1378500
Merge branch 'main' into stripe
ukrocks007 Feb 13, 2024
5702452
Merge branch 'main' into stripe
ukrocks007 Feb 14, 2024
fdfc36d
Merge branch 'main' into stripe
deepakprabhakara Feb 15, 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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ FEATURE_TEAM_AUDIT_LOG=true
FEATURE_TEAM_WEBHOOK=true
FEATURE_TEAM_API_KEY=true
FEATURE_TEAM_DELETION=true
FEATURE_TEAM_PAYMENTS=true

# Google reCAPTCHA
RECAPTCHA_SITE_KEY=
Expand All @@ -99,3 +100,10 @@ MAX_LOGIN_ATTEMPTS=5

# Set this to receive Slack notifications, https://hooks.slack.com/services/xxx/xxx/xxx
SLACK_WEBHOOK_URL=

# Stripe
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=

# Support URL
NEXT_PUBLIC_SUPPORT_URL=
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ module.exports = {
overrides: [
{
files: ['*.js'],
rules: {
'@typescript-eslint/no-var-requires': 'off', // Disable the rule for JavaScript files
},
},
{
files: [
Expand Down
33 changes: 33 additions & 0 deletions components/billing/Help.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Link from 'next/link';
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline';
import { useTranslation } from 'next-i18next';

import { Card } from '@/components/shared';

const Help = () => {
const { t } = useTranslation('common');

return (
<Card>
<Card.Body>
<Card.Header>
<Card.Title>{t('need-anything-else')}</Card.Title>
<Card.Description>{t('billing-assistance-message')}</Card.Description>
</Card.Header>
<div>
<Link
href={process.env.NEXT_PUBLIC_SUPPORT_URL || ''}
className="btn btn-primary btn-outline btn-sm"
target="_blank"
rel="noopener noreferrer"
>
{t('contact-support')}
<ArrowTopRightOnSquareIcon className="w-5 h-5 ml-2" />
</Link>
</div>
</Card.Body>
</Card>
);
};

export default Help;
68 changes: 68 additions & 0 deletions components/billing/LinkToPortal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import toast from 'react-hot-toast';
import { Button } from 'react-daisyui';
import { useState } from 'react';
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline';
import { useTranslation } from 'next-i18next';

import { Card } from '@/components/shared';
import { Team } from '@prisma/client';
import { defaultHeaders } from '@/lib/common';
import type { ApiResponse } from 'types';

interface LinkToPortalProps {
team: Team;
}

const LinkToPortal = ({ team }: LinkToPortalProps) => {
const [loading, setLoading] = useState(false);
const { t } = useTranslation('common');

const openStripePortal = async () => {
setLoading(true);

const response = await fetch(
`/api/teams/${team.slug}/payments/create-portal-link`,
{
method: 'POST',
headers: defaultHeaders,
credentials: 'same-origin',
}
);

const result = (await response.json()) as ApiResponse<{ url: string }>;

if (!response.ok) {
toast.error(result.error.message);
return;
}

setLoading(false);
window.open(result.data.url, '_blank', 'noopener,noreferrer');
};

return (
<Card>
<Card.Body>
<Card.Header>
<Card.Title>{t('manage-subscription')}</Card.Title>
<Card.Description>{t('manage-billing-information')}</Card.Description>
</Card.Header>
<div>
<Button
type="button"
color="primary"
size="sm"
variant="outline"
loading={loading}
onClick={() => openStripePortal()}
>
{t('billing-portal')}
<ArrowTopRightOnSquareIcon className="w-5 h-5 ml-2" />
</Button>
</div>
</Card.Body>
</Card>
);
};

export default LinkToPortal;
55 changes: 55 additions & 0 deletions components/billing/PaymentButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Button } from 'react-daisyui';
import getSymbolFromCurrency from 'currency-symbol-map';

import { Price, Prisma, Service } from '@prisma/client';

interface PaymentButtonProps {
plan: Service;
price: Price;
initiateCheckout: (priceId: string, quantity?: number) => void;
}

const PaymentButton = ({
plan,
price,
initiateCheckout,
}: PaymentButtonProps) => {
const metadata = price.metadata as Prisma.JsonObject;
const currencySymbol = getSymbolFromCurrency(price.currency || 'USD');
let buttonText = 'Get Started';

if (metadata?.interval === 'month') {
buttonText = price.amount
? `${currencySymbol}${price.amount} / month`
: `Monthly`;
} else if (metadata?.interval === 'year') {
buttonText = price.amount
? `${currencySymbol}${price.amount} / year`
: `Yearly`;
}

return (
<Button
key={`${plan.id}-${price.id}`}
color="primary"
variant="outline"
size="md"
fullWidth
onClick={() => {
initiateCheckout(
price.id,
(price.billingScheme == 'per_unit' ||
price.billingScheme == 'tiered') &&
metadata.usage_type !== 'metered'
? 1
: undefined
);
}}
className="rounded-full"
>
{buttonText}
</Button>
);
};

export default PaymentButton;
120 changes: 120 additions & 0 deletions components/billing/ProductPricing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import router from 'next/router';
import toast from 'react-hot-toast';
import { Button } from 'react-daisyui';
import { useTranslation } from 'next-i18next';

import useTeam from 'hooks/useTeam';
import { Price } from '@prisma/client';
import PaymentButton from './PaymentButton';
import { Service, Subscription } from '@prisma/client';

interface ProductPricingProps {
plans: any[];
subscriptions: (Subscription & { product: Service })[];
}

const ProductPricing = ({ plans, subscriptions }: ProductPricingProps) => {
const { team } = useTeam();
const { t } = useTranslation('common');

const initiateCheckout = async (priceId: string, quantity?: number) => {
const res = await fetch(
`/api/teams/${team?.slug}/payments/create-checkout-session`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ priceId, quantity }),
}
);

const data = await res.json();

if (data?.data?.url) {
router.push(data.data.url);
} else {
toast.error(
data?.error?.message ||
data?.error?.raw?.message ||
t('stripe-checkout-fallback-error')
);
}
};

const hasActiveSubscription = (price: Price) =>
subscriptions.some((s) => s.priceId === price.id);

return (
<section className="py-3">
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
{plans.map((plan) => {
return (
<div
className="relative rounded-md bg-white border border-gray-200"
key={plan.id}
>
<div className="p-8">
<div className="flex items-center space-x-2">
<h3 className="font-display text-2xl font-bold text-black">
{plan.name}
</h3>
</div>
<p className="mt-2 text-gray-500 h-10">{plan.description}</p>
</div>
<div className="flex justify-center flex-col gap-2 border-b border-t border-gray-200 bg-gray-50 px-8 py-5 h-32">
{plan.prices.map((price: Price) =>
hasActiveSubscription(price) ? (
<Button
key={price.id}
variant="outline"
size="md"
fullWidth
disabled
className="rounded-full"
>
{t('current')}
</Button>
) : (
<PaymentButton
key={price.id}
plan={plan}
price={price}
initiateCheckout={initiateCheckout}
/>
)
)}
</div>
<ul className="mb-10 mt-5 space-y-4 px-8">
{plan.features.map((feature: string) => (
<li className="flex space-x-4" key={`${plan.id}-${feature}`}>
<svg
className="h-6 w-6 flex-none text-black"
viewBox="0 0 24 24"
width={24}
height={24}
fill="none"
shapeRendering="geometricPrecision"
>
<path
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2Z"
fill="currentColor"
/>
<path
d="M8 11.8571L10.5 14.3572L15.8572 9"
stroke="white"
/>
</svg>
<p className="text-gray-600">{feature}</p>
</li>
))}
</ul>
</div>
);
})}
</div>
</section>
);
};

export default ProductPricing;
45 changes: 45 additions & 0 deletions components/billing/Subscriptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useTranslation } from 'next-i18next';

import { Service, Subscription } from '@prisma/client';

interface SubscriptionsProps {
subscriptions: (Subscription & { product: Service })[];
}

const Subscriptions = ({ subscriptions }: SubscriptionsProps) => {
const { t } = useTranslation('common');

if (subscriptions.length === 0) {
return null;
}

return (
<div className="space-y-3">
<h2 className="card-title text-xl font-medium leading-none tracking-tight">
{t('subscriptions')}
</h2>
<table className="table w-full text-sm border">
<thead>
<tr>
<th>ID</th>
<th>{t('plan')}</th>
<th>{t('start-date')}</th>
<th>{t('end-date')}</th>
</tr>
</thead>
<tbody>
{subscriptions.map((subscription) => (
<tr key={subscription.id}>
<td>{subscription.id}</td>
<td>{subscription.product.name}</td>
<td>{new Date(subscription.startDate).toLocaleDateString()}</td>
<td>{new Date(subscription.endDate).toLocaleDateString()}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};

export default Subscriptions;
13 changes: 13 additions & 0 deletions components/team/TeamTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
PaperAirplaneIcon,
ShieldExclamationIcon,
UserPlusIcon,
BanknotesIcon,
} from '@heroicons/react/24/outline';
import type { Team } from '@prisma/client';
import classNames from 'classnames';
Expand Down Expand Up @@ -76,6 +77,18 @@ const TeamTab = ({ activeTab, team, heading, teamFeatures }: TeamTabProps) => {
});
}

if (
teamFeatures.payments &&
canAccess('team_payments', ['create', 'update', 'read', 'delete'])
) {
navigations.push({
name: 'Billing',
href: `/teams/${team.slug}/billing`,
active: activeTab === 'payments',
icon: BanknotesIcon,
});
}

if (
teamFeatures.webhook &&
canAccess('team_webhook', ['create', 'update', 'read', 'delete'])
Expand Down
Loading