-
+ {user?.allowTrial && (
+
-
-
-
+
+
+
Pay nothing for the first 7 days
-
Pay nothing for the first 7 days
-
-
-
-
+
+
+
Cancel anytime, hassle-free
-
Cancel anytime, hassle-free
-
+ )}
>
diff --git a/apps/frontend/src/components/layout/user.context.tsx b/apps/frontend/src/components/layout/user.context.tsx
index 88d19fd3..d6be4e9e 100644
--- a/apps/frontend/src/components/layout/user.context.tsx
+++ b/apps/frontend/src/components/layout/user.context.tsx
@@ -17,6 +17,7 @@ export const UserContext = createContext<
totalChannels: number;
isLifetime?: boolean;
impersonate: boolean;
+ allowTrial: boolean;
})
>(undefined);
diff --git a/apps/frontend/tailwind.config.js b/apps/frontend/tailwind.config.js
index 32b81732..50a646ac 100644
--- a/apps/frontend/tailwind.config.js
+++ b/apps/frontend/tailwind.config.js
@@ -131,11 +131,11 @@ module.exports = {
'100%': { overflow: 'hidden' },
},
fadeDown: {
- '0%': { opacity: 0, transform: 'translateY(-30px)' },
- '10%': { opacity: 1, transform: 'translateY(0)' },
- '85%': { opacity: 1, transform: 'translateY(0)' },
- '90%': { opacity: 1, transform: 'translateY(10px)' },
- '100%': { opacity: 0, transform: 'translateY(-30px)' },
+ '0%': { opacity: 0, marginTop: -30},
+ '10%': { opacity: 1, marginTop: 0 },
+ '85%': { opacity: 1, marginTop: 0 },
+ '90%': { opacity: 1, marginTop: 10 },
+ '100%': { opacity: 0, marginTop: -30 },
},
normalFadeDown: {
'0%': { opacity: 0, transform: 'translateY(-30px)' },
diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts
index 4da999a9..ed5c5f07 100644
--- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts
@@ -215,6 +215,7 @@ export class OrganizationRepository {
data: {
name: body.company,
apiKey: AuthService.fixedEncryption(makeId(20)),
+ allowTrial: true,
users: {
create: {
role: Role.SUPERADMIN,
@@ -246,6 +247,14 @@ export class OrganizationRepository {
});
}
+ getOrgByCustomerId(customerId: string) {
+ return this._organization.model.organization.findFirst({
+ where: {
+ paymentId: customerId,
+ },
+ });
+ }
+
async getTeam(orgId: string) {
return this._organization.model.organization.findUnique({
where: {
diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts
index 37235d61..48dec476 100644
--- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts
@@ -60,6 +60,10 @@ export class OrganizationService {
return this._organizationRepository.getTeam(orgId);
}
+ getOrgByCustomerId(customerId: string) {
+ return this._organizationRepository.getOrgByCustomerId(customerId);
+ }
+
async inviteTeamMember(orgId: string, body: AddTeamMemberDto) {
const timeLimit = dayjs().add(1, 'hour').format('YYYY-MM-DD HH:mm:ss');
const id = makeId(5);
diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
index 088c2a0f..460b6363 100644
--- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma
+++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
@@ -25,6 +25,7 @@ model Organization {
Integration Integration[]
post Post[] @relation("organization")
submittedPost Post[] @relation("submittedForOrg")
+ allowTrial Boolean @default(false)
Comments Comments[]
notifications Notifications[]
buyerOrganization MessagesGroup[]
diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts
index 04ccb431..6fc9517c 100644
--- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts
@@ -64,6 +64,17 @@ export class SubscriptionRepository {
});
}
+ getCustomerIdByOrgId(organizationId: string) {
+ return this._organization.model.organization.findFirst({
+ where: {
+ id: organizationId,
+ },
+ select: {
+ paymentId: true,
+ },
+ });
+ }
+
checkSubscription(organizationId: string, subscriptionId: string) {
return this._subscription.model.subscription.findFirst({
where: {
@@ -158,6 +169,15 @@ export class SubscriptionRepository {
},
});
+ await this._organization.model.organization.update({
+ where: {
+ id: findOrg.id,
+ },
+ data: {
+ allowTrial: false,
+ },
+ });
+
if (code) {
await this._usedCodes.model.usedCodes.create({
data: {
diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts
index 4f11c0f9..6b5ae00d 100644
--- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts
@@ -6,13 +6,14 @@ import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/o
import { Organization } from '@prisma/client';
import dayjs from 'dayjs';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
+import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
@Injectable()
export class SubscriptionService {
constructor(
private readonly _subscriptionRepository: SubscriptionRepository,
private readonly _integrationService: IntegrationService,
- private readonly _organizationService: OrganizationService
+ private readonly _organizationService: OrganizationService,
) {}
getSubscriptionByOrganizationId(organizationId: string) {
@@ -55,8 +56,8 @@ export class SubscriptionService {
);
}
- checkSubscription(organizationId: string, subscriptionId: string) {
- return this._subscriptionRepository.checkSubscription(
+ async checkSubscription(organizationId: string, subscriptionId: string) {
+ return await this._subscriptionRepository.checkSubscription(
organizationId,
subscriptionId
);
@@ -197,9 +198,7 @@ export class SubscriptionService {
'MONTHLY',
null,
undefined,
- orgId
+ orgId
);
}
-
-
}
diff --git a/libraries/nestjs-libraries/src/services/stripe.service.ts b/libraries/nestjs-libraries/src/services/stripe.service.ts
index 347995e4..5d7a2e12 100644
--- a/libraries/nestjs-libraries/src/services/stripe.service.ts
+++ b/libraries/nestjs-libraries/src/services/stripe.service.ts
@@ -45,20 +45,79 @@ export class StripeService {
);
}
- createSubscription(event: Stripe.CustomerSubscriptionCreatedEvent) {
+ async checkValidCard(
+ event:
+ | Stripe.CustomerSubscriptionCreatedEvent
+ | Stripe.CustomerSubscriptionUpdatedEvent
+ ) {
+ if (event.data.object.status === 'incomplete') {
+ return false;
+ }
+
+ const getOrgFromCustomer = await this._organizationService.getOrgByCustomerId(event.data.object.customer as string);
+
+ if (!getOrgFromCustomer?.allowTrial) {
+ return true;
+ }
+
+ console.log('Checking card');
+
+ const paymentMethods = await stripe.paymentMethods.list({
+ customer: event.data.object.customer as string,
+ });
+
+ // find the last one created
+ const latestMethod = paymentMethods.data.reduce((prev, current) => {
+ if (prev.created < current.created) {
+ return current;
+ }
+ return prev;
+ });
+
+ const paymentIntent = await stripe.paymentIntents.create({
+ amount: 100,
+ currency: 'usd',
+ payment_method: latestMethod.id,
+ customer: event.data.object.customer as string,
+ automatic_payment_methods: {
+ allow_redirects: 'never',
+ enabled: true,
+ },
+ capture_method: 'manual', // Authorize without capturing
+ confirm: true, // Confirm the PaymentIntent
+ });
+
+ if (paymentIntent.status !== 'requires_capture') {
+ console.error('Cant charge');
+ await stripe.paymentMethods.detach(paymentMethods.data[0].id);
+ await stripe.subscriptions.cancel(event.data.object.id as string);
+ return false;
+ }
+
+ await stripe.paymentIntents.cancel(paymentIntent.id as string);
+ return true;
+ }
+
+ async createSubscription(event: Stripe.CustomerSubscriptionCreatedEvent) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const {
- id,
+ uniqueId,
billing,
period,
}: {
billing: 'STANDARD' | 'PRO';
period: 'MONTHLY' | 'YEARLY';
- id: string;
+ uniqueId: string;
} = event.data.object.metadata;
+
+ const check = await this.checkValidCard(event);
+ if (!check) {
+ return { ok: false };
+ }
+
return this._subscriptionService.createOrUpdateSubscription(
- id,
+ uniqueId,
event.data.object.customer as string,
pricing[billing].channel!,
billing,
@@ -66,20 +125,26 @@ export class StripeService {
event.data.object.cancel_at
);
}
- updateSubscription(event: Stripe.CustomerSubscriptionUpdatedEvent) {
+ async updateSubscription(event: Stripe.CustomerSubscriptionUpdatedEvent) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const {
- id,
+ uniqueId,
billing,
period,
}: {
billing: 'STANDARD' | 'PRO';
period: 'MONTHLY' | 'YEARLY';
- id: string;
+ uniqueId: string;
} = event.data.object.metadata;
+
+ const check = await this.checkValidCard(event);
+ if (!check) {
+ return { ok: false };
+ }
+
return this._subscriptionService.createOrUpdateSubscription(
- id,
+ uniqueId,
event.data.object.customer as string,
pricing[billing].channel!,
billing,
@@ -218,6 +283,15 @@ export class StripeService {
}
}
+ async getCustomerSubscriptions(organizationId: string) {
+ const org = (await this._organizationService.getOrgById(organizationId))!;
+ const customer = org.paymentId;
+ return stripe.subscriptions.list({
+ customer: customer!,
+ status: 'all',
+ });
+ }
+
async setToCancel(organizationId: string) {
const id = makeId(10);
const org = await this._organizationService.getOrgById(organizationId);
@@ -228,7 +302,7 @@ export class StripeService {
customer,
status: 'all',
})
- ).data,
+ ).data.filter((f) => f.status !== 'canceled'),
};
const { cancel_at } = await stripe.subscriptions.update(
@@ -275,7 +349,8 @@ export class StripeService {
customer: string,
body: BillingSubscribeDto,
price: string,
- userId: string
+ userId: string,
+ allowTrial: boolean
) {
const isUtm = body.utm ? `&utm_source=${body.utm}` : '';
const { url } = await stripe.checkout.sessions.create({
@@ -286,7 +361,7 @@ export class StripeService {
`/launches?onboarding=true&check=${uniqueId}${isUtm}`,
mode: 'subscription',
subscription_data: {
- trial_period_days: 7,
+ ...(allowTrial ? { trial_period_days: 7 } : {}),
metadata: {
service: 'gitroom',
...body,
@@ -370,6 +445,34 @@ export class StripeService {
return accountLink.url;
}
+ async checkSubscription(organizationId: string, subscriptionId: string) {
+ const orgValue = await this._subscriptionService.checkSubscription(
+ organizationId,
+ subscriptionId
+ );
+
+ if (orgValue) {
+ return 2;
+ }
+
+ const getCustomerSubscriptions = await this.getCustomerSubscriptions(
+ organizationId
+ );
+ if (getCustomerSubscriptions.data.length === 0) {
+ return 0;
+ }
+
+ if (
+ getCustomerSubscriptions.data.find(
+ (p) => p.metadata.uniqueId === subscriptionId
+ )?.canceled_at
+ ) {
+ return 1;
+ }
+
+ return 0;
+ }
+
async payAccountStepOne(
userId: string,
organization: Organization,
@@ -431,7 +534,8 @@ export class StripeService {
uniqueId: string,
organizationId: string,
userId: string,
- body: BillingSubscribeDto
+ body: BillingSubscribeDto,
+ allowTrial: boolean
) {
const id = makeId(10);
const priceData = pricing[body.billing];
@@ -481,6 +585,21 @@ export class StripeService {
},
}));
+ const getCurrentSubscriptions =
+ await this._subscriptionService.getSubscription(organizationId);
+
+ if (!getCurrentSubscriptions) {
+ return this.createCheckoutSession(
+ uniqueId,
+ id,
+ customer,
+ body,
+ findPrice!.id,
+ userId,
+ allowTrial
+ );
+ }
+
const currentUserSubscription = {
data: (
await stripe.subscriptions.list({
@@ -490,17 +609,6 @@ export class StripeService {
).data.filter((f) => f.status === 'active' || f.status === 'trialing'),
};
- if (!currentUserSubscription.data.length) {
- return this.createCheckoutSession(
- uniqueId,
- id,
- customer,
- body,
- findPrice!.id,
- userId
- );
- }
-
try {
await stripe.subscriptions.update(currentUserSubscription.data[0].id, {
cancel_at_period_end: false,