Skip to content

Commit

Permalink
added welcome email after subscription
Browse files Browse the repository at this point in the history
  • Loading branch information
imolorhe committed Oct 28, 2024
1 parent a8e69c1 commit 2581b18
Show file tree
Hide file tree
Showing 13 changed files with 862 additions and 122 deletions.
2 changes: 2 additions & 0 deletions packages/altair-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dependencies": {
"@altairgraphql/api-utils": "^8.0.0",
"@altairgraphql/db": "^8.0.0",
"@altairgraphql/emails": "^8.0.0",
"@langchain/anthropic": "^0.2.4",
"@langchain/community": "^0.2.19",
"@langchain/openai": "^0.2.2",
Expand Down Expand Up @@ -33,6 +34,7 @@
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.1.13",
"resend": "^4.0.0",
"rimraf": "^5.0.5",
"rxjs": "^7.8.1",
"stripe": "^16.2.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/altair-api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ScheduleModule } from '@nestjs/schedule';
import { WinstonModule, utilities } from 'nest-winston';
import { format, transports } from 'winston';
import { AiModule } from './ai/ai.module';
import { EmailService } from './email/email.service';

@Module({
imports: [
Expand Down Expand Up @@ -59,6 +60,6 @@ import { AiModule } from './ai/ai.module';
AiModule,
],
controllers: [AppController, StripeWebhookController],
providers: [AppService, PasswordService],
providers: [AppService, PasswordService, EmailService],
})
export class AppModule {}
5 changes: 5 additions & 0 deletions packages/altair-api/src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ const config = {
model: process.env.ANTHROPIC_MODEL_NAME ?? 'claude-3-haiku-20240307',
},
},
email: {
resendApiKey: process.env.RESEND_API_KEY,
defaultFrom: 'Altair GraphQL <no-reply@mail.altairgraphql.dev>',
replyTo: 'reply@mail.altairgraphql.dev',
},
};

export type Config = typeof config;
Expand Down
18 changes: 18 additions & 0 deletions packages/altair-api/src/email/email.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EmailService } from './email.service';

describe('EmailService', () => {
let service: EmailService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [EmailService],
}).compile();

service = module.get<EmailService>(EmailService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
28 changes: 28 additions & 0 deletions packages/altair-api/src/email/email.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Resend } from 'resend';
import { renderWelcomeEmail } from '@altairgraphql/emails';

@Injectable()
export class EmailService {
private resend: Resend;
constructor(private configService: ConfigService) {
this.resend = new Resend(this.configService.get('email.resendApiKey'));
}

async sendWelcomeEmail(email: string, username: string) {
const { data, error } = await this.resend.emails.send({
from:
this.configService.get('email.defaultFrom') ?? 'info@mail.altairgraphql.dev',
to: email,
replyTo: this.configService.get('email.replyTo'),
subject: 'Welcome to Altair GraphQL Cloud',
html: await renderWelcomeEmail({ username }),
});
if (error) {
console.error('Error sending welcome email', error);
}

return { data, error };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { BadRequestException, Controller, Header, Post, Req } from '@nestjs/comm
import { Request } from 'express';
import { PrismaService } from 'nestjs-prisma';
import { UserService } from 'src/auth/user/user.service';
import { EmailService } from 'src/email/email.service';
import { StripeService } from 'src/stripe/stripe.service';
import { Stripe } from 'stripe';

Expand All @@ -16,7 +17,8 @@ export class StripeWebhookController {
constructor(
private readonly stripeService: StripeService,
private readonly userService: UserService,
private readonly prisma: PrismaService
private readonly prisma: PrismaService,
private readonly emailService: EmailService
) {
// TODO: Synchronise with Stripe on startup
}
Expand Down Expand Up @@ -70,12 +72,18 @@ export class StripeWebhookController {
await this.userService.toBasicPlan(user.id);
} else if (planRole === PRO_PLAN_ID) {
await this.userService.toProPlan(user.id, quantity);
// Send welcome email
console.log('Sending welcome email');
await this.emailService.sendWelcomeEmail(
user.email,
user.firstName ?? user.email
);
}
break;
}
case 'checkout.session.completed':
case 'checkout.session.async_payment_succeeded': {
// TODO: Handle credit purchase
// Handle credit purchase
const data: Stripe.Checkout.Session = event.data.object;
const checkoutSession = await this.stripeService.retrieveCheckoutSession(
data.id
Expand Down
5 changes: 5 additions & 0 deletions packages/altair-api/src/stripe/stripe.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@ export class StripeService {
);
}

if (res.data.length === 0) {
console.error('Customer does not have an active subscription. Exiting.');
return;
}

// update first item only
const itemId = res.data.at(0)?.items.data.at(0)?.id;

Expand Down
4 changes: 2 additions & 2 deletions packages/altair-koa-middleware/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
"@types/koa": "^2.13.4",
"@types/koa-send": "^4.1.3",
"@types/koa__router": "^8.0.8",
"@types/supertest": "^2.0.11",
"@types/supertest": "^6.0.2",
"jest": "29.4.1",
"koa": "^2.13.1",
"nodemon": "^2.0.12",
"supertest": "^6.1.6",
"supertest": "^7.0.0",
"ts-jest": "29.0.5",
"ts-node": "^10.2.1",
"typescript": "5.2.2"
Expand Down
20 changes: 20 additions & 0 deletions packages/transactional/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@altairgraphql/emails",
"version": "8.0.0",
"main": "dist/index.js",
"license": "MIT",
"dependencies": {
"@react-email/components": "^0.0.25"
},
"devDependencies": {
"@types/html-to-text": "^9.0.4",
"@types/prismjs": "^1.26.5",
"react-email": "^3.0.1",
"typescript": "5.2.2"
},
"scripts": {
"build": "tsc",
"dev": "email dev",
"prepare": "yarn build"
}
}
128 changes: 128 additions & 0 deletions packages/transactional/src/emails/Welcome.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Img,
Link,
Preview,
Row,
Section,
Text,
Tailwind,
} from '@react-email/components';
import * as React from 'react';

export interface WelcomeEmailProps {
username: string;
}

const PropDefaults: WelcomeEmailProps = {
username: 'User',
};

export const WelcomeEmail = ({
username = PropDefaults.username,
}: WelcomeEmailProps) => {
return (
<Html>
<Head />
<Preview>Welcome to Altair GraphQL Cloud</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: {
brand: '#64CB29',
offwhite: '#fafbfb',
},
spacing: {
0: '0px',
20: '20px',
45: '45px',
},
},
},
}}
>
<Body className="bg-offwhite text-base font-sans">
<Img
src={`https://altairgraphql.dev/assets/img/altair_logo_128.png`}
width="100"
height="100"
alt="Altair GraphQL Cloud"
className="mx-auto my-20"
/>
<Container className="bg-white p-45">
<Heading className="text-center my-0 leading-8">
Welcome to Altair GraphQL Cloud
</Heading>

<Section>
<Row>
<Text className="text-base">Hey {username}! 👋🏾</Text>

<Text className="text-base">
I'm Samuel, the creator of Altair GraphQL Client. Thanks so much
for subscribing to Altair GraphQL Cloud!
</Text>

<Text className="text-base">
I'd love to hear what made you choose Altair Premium and what
features you're most excited about. Just hit reply and let me know
your thoughts!
</Text>

<Text className="text-base">
In the meantime, you can get started by checking out the docs
</Text>
</Row>
</Section>

{/* <ul>{steps?.map(({ Description }) => Description)}</ul> */}

<Section className="text-center">
<Button
className="bg-brand text-white rounded-md py-3 px-[18px] block"
href="https://altairgraphql.dev/docs/cloud/"
>
Get Started
</Button>
</Section>

{/* <Section className="mt-45">
<Row>
{links?.map((link) => (
<Column key={link}>
<Link className="text-black underline font-bold">{link}</Link>{' '}
<span className="text-green-500">→</span>
</Column>
))}
</Row>
</Section> */}
</Container>

{/* <Container className="mt-20">
<Section>
<Row>
<Column className="text-right px-20">
<Link>Unsubscribe</Link>
</Column>
<Column className="text-left">
<Link>Manage Preferences</Link>
</Column>
</Row>
</Section>
<Text className="text-center text-gray-400 mb-45">
Netlify, 44 Montgomery Street, Suite 300 San Francisco, CA
</Text>
</Container> */}
</Body>
</Tailwind>
</Html>
);
};

export default WelcomeEmail;
6 changes: 6 additions & 0 deletions packages/transactional/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Options, render } from '@react-email/components';
import { WelcomeEmail, WelcomeEmailProps } from './emails/Welcome';

export const renderWelcomeEmail = (props: WelcomeEmailProps, options?: Options) => {
return render(<WelcomeEmail {...props} />, options);
};
15 changes: 15 additions & 0 deletions packages/transactional/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"include": ["."],
"exclude": ["dist", "build", "node_modules"],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"strict": true,
"declaration": true,
"sourceMap": true,
"jsx": "react-jsx",
"esModuleInterop": true,
"lib": ["es2022", "dom", "DOM.Iterable"],
"skipLibCheck": true
}
}
Loading

0 comments on commit 2581b18

Please sign in to comment.