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 19 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
5 changes: 5 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,7 @@ 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=
131 changes: 131 additions & 0 deletions components/payments/productPricing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { CheckIcon } from '@heroicons/react/20/solid';
import { useTranslation } from 'next-i18next';
import { Button, Card } from 'react-daisyui';
import Image from 'next/image';
import useTeam from 'hooks/useTeam';
import router from 'next/router';
import toast from 'react-hot-toast';

const ProductPricing = ({
plans,
disabledPrices,
}: {
plans: any[];
disabledPrices: string[];
}) => {
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')
);
}
};
return (
<section className="py-6">
<div className="flex flex-col justify-center space-y-6">
<h2 className="text-center text-4xl font-bold normal-case">
{t('pricing')}
</h2>
<div className="flex items-center justify-center">
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
{plans.map((plan, index) => {
return (
<Card
key={`plan-${index}`}
className="rounded-md dark:border-gray-200 border border-gray-300"
>
<Card.Body>
<Card.Title tag="h2">
<h2 className="mx-auto">{plan.name}</h2>
</Card.Title>
<Image
className="mx-auto"
src={plan.image || ''}
alt={plan.name}
width={100}
height={100}
/>
<p>{plan.description}</p>
<div className="mt-5">
<ul className="flex flex-col space-y-2">
{plan.features.map(
(feature: any, itemIndex: number) => {
return (
<li
key={`plan-${index}-benefit-${itemIndex}`}
className="flex items-center"
>
<CheckIcon className="h-5 w-5" />
<span className="ml-1">{feature}</span>
</li>
);
}
)}
</ul>
</div>
</Card.Body>
<Card.Actions className="justify-center m-2">
{(plan?.prices || [])
.sort(
(a, b) => a.recurring.interval < b.recurring.interval
)
.map((price: any, priceIndex: number) => {
return (
<Button
key={`plan-${index}-price-${priceIndex}`}
color="primary"
className="md:w-full w-3/4 rounded-md"
size="md"
disabled={disabledPrices.includes(price.id)}
onClick={() => {
initiateCheckout(
price.id,
(price.billingScheme == 'per_unit' ||
price.billingScheme == 'tiered') &&
price.recurring.usage_type !== 'metered'
? 1
: undefined
);
}}
>
{price?.recurring?.interval
? `${
price?.recurring?.interval === 'month'
? 'Monthly Plan'
: 'Yearly Plan'
} `
: 'Buy Now'}
</Button>
);
})}
</Card.Actions>
</Card>
);
})}
</div>
</div>
</div>
</section>
);
};

export default ProductPricing;
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: 'Payments',
ukrocks007 marked this conversation as resolved.
Show resolved Hide resolved
href: `/teams/${team.slug}/payments`,
active: activeTab === 'payments',
icon: BanknotesIcon,
});
}

if (
teamFeatures.webhook &&
canAccess('team_webhook', ['create', 'update', 'read', 'delete'])
Expand Down
6 changes: 6 additions & 0 deletions lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ const env = {
webhook: process.env.FEATURE_TEAM_WEBHOOK === 'false' ? false : true,
apiKey: process.env.FEATURE_TEAM_API_KEY === 'false' ? false : true,
auditLog: process.env.FEATURE_TEAM_AUDIT_LOG === 'false' ? false : true,
payments: process.env.FEATURE_TEAM_PAYMENTS === 'false' ? false : true,
deleteTeam: process.env.FEATURE_TEAM_DELETION === 'false' ? false : true,
},

Expand All @@ -107,6 +108,11 @@ const env = {
maxLoginAttempts: Number(process.env.MAX_LOGIN_ATTEMPTS) || 5,

slackWebhookUrl: process.env.SLACK_WEBHOOK_URL,

stripe: {
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
ukrocks007 marked this conversation as resolved.
Show resolved Hide resolved
},
};

export default env;
5 changes: 5 additions & 0 deletions lib/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type Resource =
| 'team_dsync'
| 'team_audit_log'
| 'team_webhook'
| 'team_payments'
| 'team_api_key';

export type RolePermissions = {
Expand Down Expand Up @@ -62,6 +63,10 @@ export const permissions: RolePermissions = {
resource: 'team_audit_log',
actions: '*',
},
{
resource: 'team_payments',
actions: '*',
},
{
resource: 'team_webhook',
actions: '*',
Expand Down
48 changes: 48 additions & 0 deletions lib/stripe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createStripeTeam, getByTeamId } from 'models/stripeTeam';
import Stripe from 'stripe';
import env from '@/lib/env';

export const stripe = new Stripe(env.stripe.stripeSecretKey ?? '', {
// https://github.com/stripe/stripe-node#configuration
apiVersion: '2022-11-15',
// Register this as an official Stripe plugin.
// https://stripe.com/docs/building-plugins#setappinfo
appInfo: {
name: 'saas-starter-kit',
version: '0.1.0',
},
ukrocks007 marked this conversation as resolved.
Show resolved Hide resolved
});

export const getURL = () => {
ukrocks007 marked this conversation as resolved.
Show resolved Hide resolved
let url = env.appUrl || 'http://localhost:4002/';
// Make sure to include `https://` when not localhost.
url = url.includes('http') ? url : `https://${url}`;
// Make sure to including trailing `/`.
url = url.charAt(url.length - 1) === '/' ? url : `${url}/`;
return url;
};

export async function getStripeCustomerId(teamMember, session?: any) {
let customerId = '';
const stripeTeam = await getByTeamId(teamMember.teamId);
if (!stripeTeam) {
const customerData: {
metadata: { teamId: string };
email?: string;
} = {
metadata: {
teamId: teamMember.teamId,
},
};
if (session?.user?.email) customerData.email = session?.user?.email;
const customer = await stripe.customers.create({
...customerData,
name: session?.user?.name as string,
});
await createStripeTeam(teamMember.teamId, customer.id);
customerId = customer.id;
} else {
customerId = stripeTeam.customerId;
}
return customerId;
}
8 changes: 7 additions & 1 deletion locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,5 +198,11 @@
"remove-other-browser-session-warning": "Once removed you will be logged out of the app on that browser.",
"remove-current-browser-session-warning": "You are about to remove your current session. Once you do this, you will be logged out of the app.",
"session-removed": "Browser session removed.",
"remove-team-restricted": "Please contact our support if you want to remove this team. We will help you with that."
"remove-team-restricted": "Please contact our support if you want to remove this team. We will help you with that.",
"open-customer-portal": "Open Customer Portal",
"subscriptions": "Subscriptions",
"from": "From",
"to": "To",
"stripe-checkout-fallback-error": "Somthing went wrong while initiating checkout, please try again later.",
ukrocks007 marked this conversation as resolved.
Show resolved Hide resolved
"error-occurred": "An error occurred"
}
1 change: 1 addition & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const unAuthenticatedRoutes = [
'/api/oauth/**',
'/api/scim/v2.0/**',
'/api/invitations/*',
'/api/webhooks/stripe',
'/api/webhooks/dsync',
'/auth/**',
'/invitations/*',
Expand Down
6 changes: 6 additions & 0 deletions models/stripePrice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { prisma } from '@/lib/prisma';

export const getAllPrices = async () => {
const prices = await prisma.stripePrice.findMany({});
return prices;
};
6 changes: 6 additions & 0 deletions models/stripeProduct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { prisma } from '@/lib/prisma';

export const getAllProducts = async () => {
const products = await prisma.stripeProduct.findMany({});
return products;
};
53 changes: 53 additions & 0 deletions models/stripeSubscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { prisma } from '@/lib/prisma';

export const createStripeSubscription = async (
customerId: string,
subscriptionId: string,
active: boolean,
startDate: Date,
endDate: Date,
priceId: string
) => {
const stripeSubscription = await prisma.stripeSubscription.create({
data: {
customerId,
subscriptionId,
active,
startDate,
endDate,
priceId,
},
});
return stripeSubscription;
};

export const deleteStripeSubscription = async (subscriptionId: string) => {
const stripeSubscription = await prisma.stripeSubscription.deleteMany({
where: {
subscriptionId,
},
});
return stripeSubscription;
};

export const updateStripeSubscription = async (
subscriptionId: string,
data: any
) => {
const stripeSubscription = await prisma.stripeSubscription.updateMany({
where: {
subscriptionId,
},
data,
});
return stripeSubscription;
};

export const getByCustomerId = async (customerId: string) => {
const stripeSubscription = await prisma.stripeSubscription.findMany({
where: {
customerId,
},
});
return stripeSubscription;
};
27 changes: 27 additions & 0 deletions models/stripeTeam.ts
ukrocks007 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { prisma } from '@/lib/prisma';

export const createStripeTeam = async (teamId: string, customerId: string) => {
const stripeTeam = await prisma.stripeTeam.create({
data: {
teamId,
customerId,
},
});
return stripeTeam;
};

export const getByTeamId = async (teamId: string) => {
return await prisma.stripeTeam.findUnique({
where: {
teamId,
},
});
};

export const getByCustomerId = async (customerId: string) => {
return await prisma.stripeTeam.findFirstOrThrow({
where: {
customerId,
},
});
};
4 changes: 4 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const nextConfig = {
protocol: 'https',
hostname: 'boxyhq.com',
},
{
protocol: 'https',
hostname: 'files.stripe.com',
},
],
},
i18n,
Expand Down
Loading