Skip to content

Commit

Permalink
Wrap IndiePitcher in MailService (#17)
Browse files Browse the repository at this point in the history
* Wrap IndiePitcher in MailService

* better readme and option to avoid having to generate indiepitcher api key
  • Loading branch information
petrpavlik authored Dec 17, 2024
1 parent f62d53d commit e69c2fc
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 140 deletions.
60 changes: 30 additions & 30 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-server/async-http-client.git",
"state" : {
"revision" : "0a9b72369b9d87ab155ef585ef50700a34abf070",
"version" : "1.23.1"
"revision" : "2119f0d9cc1b334e25447fe43d3693c0e60e6234",
"version" : "1.24.0"
}
},
{
Expand Down Expand Up @@ -78,35 +78,35 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/IndiePitcher/indiepitcher-swift.git",
"state" : {
"revision" : "6ffaacb99800a448653d7537d6e57cf27c70525c",
"version" : "1.1.0"
"revision" : "5e0a02c8c29f1b2c6cc2fcbd65c215baca14f8d2",
"version" : "1.2.3"
}
},
{
"identity" : "jwt",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/jwt.git",
"state" : {
"revision" : "8ce7280a77ca45711f1b08bc1abfac7a2bb9c97b",
"version" : "5.1.0"
"revision" : "ec5a9d489a73560732c7a27ce860fe799b1413be",
"version" : "5.1.1"
}
},
{
"identity" : "jwt-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/jwt-kit.git",
"state" : {
"revision" : "02a0fa600eee1bdc892013d62fc795fc623a5cc3",
"version" : "5.1.0"
"revision" : "6f745e91e2422608fe14c9a66ee3826cb661e2a6",
"version" : "5.1.1"
}
},
{
"identity" : "mixpanelvapor",
"kind" : "remoteSourceControl",
"location" : "https://github.com/petrpavlik/MixpanelVapor.git",
"state" : {
"revision" : "7f18c3a7b270391d2ea51ea87a56eef0d60134d2",
"version" : "1.0.0"
"revision" : "09de904adffe3044f4e9e397826fcb2d3c7802a7",
"version" : "1.1.2"
}
},
{
Expand All @@ -123,8 +123,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Quick/Nimble.git",
"state" : {
"revision" : "6416749c3c0488664fff6b42f8bf3ea8dc282ca1",
"version" : "13.6.0"
"revision" : "7795df4fff1a9cd231fe4867ae54f4dc5f5734f9",
"version" : "13.7.1"
}
},
{
Expand All @@ -141,8 +141,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/postgres-nio.git",
"state" : {
"revision" : "cd5318a01a1efcb1e0b3c82a0ce5c9fefaf1cb2d",
"version" : "1.22.1"
"revision" : "fd0e415a705c490499f983639b04f491a2ed9d99",
"version" : "1.23.0"
}
},
{
Expand Down Expand Up @@ -186,8 +186,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms.git",
"state" : {
"revision" : "5c8bd186f48c16af0775972700626f0b74588278",
"version" : "1.0.2"
"revision" : "4c3ea81f81f0a25d0470188459c6d4bf20cf2f97",
"version" : "1.0.3"
}
},
{
Expand Down Expand Up @@ -222,26 +222,26 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
"revision" : "21f7878f2b39d46fd8ba2b06459ccb431cdf876c",
"version" : "3.8.1"
"revision" : "ff0f781cf7c6a22d52957e50b104f5768b50c779",
"version" : "3.10.0"
}
},
{
"identity" : "swift-http-types",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-types",
"state" : {
"revision" : "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd",
"version" : "1.3.0"
"revision" : "ef18d829e8b92d731ad27bb81583edd2094d1ce3",
"version" : "1.3.1"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "9cb486020ebf03bfa5b5df985387a14a98744537",
"version" : "1.6.1"
"revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91",
"version" : "1.6.2"
}
},
{
Expand All @@ -258,8 +258,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "914081701062b11e3bb9e21accc379822621995e",
"version" : "2.76.1"
"revision" : "dca6594f65308c761a9c409e09fbf35f48d50d34",
"version" : "2.77.0"
}
},
{
Expand All @@ -285,8 +285,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
"revision" : "d7ceaf0e4d8001cd35cdc12e42cdd281e9e564e8",
"version" : "2.28.0"
"revision" : "c7e95421334b1068490b5d41314a50e70bab23d1",
"version" : "2.29.0"
}
},
{
Expand Down Expand Up @@ -321,8 +321,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-server/swift-service-lifecycle.git",
"state" : {
"revision" : "24c800fb494fbee6e42bc156dc94232dc08971af",
"version" : "2.6.1"
"revision" : "f70b838872863396a25694d8b19fe58bcd0b7903",
"version" : "2.6.2"
}
},
{
Expand All @@ -348,8 +348,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/vapor.git",
"state" : {
"revision" : "4d3bc6ce08b72a14c9879810cf0be455ca98f1fb",
"version" : "4.106.1"
"revision" : "e1002f35edf92e2a579580f2d1df92e01287c6c7",
"version" : "4.108.0"
}
},
{
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ Every SAAS needs to handle user sign up, and if your service takes off, you'll s
- When cloned, create `.env` file and fill in following info to be able to run the app against a local database.
- ```
FIREBASE_PROJECT_ID=your-firebase-project-id
IP_SECRET_API_KEY=your-indiepitcher-api-key
```
- This is enough to run the project locally. When deploying to production, you'll want to add the database connection keys, as well as optionally your mixpanel and sentry credentials
- You can copy the `FIREBASE_PROJECT_ID` from `.env.testing` to try things out, but please do create your own firebase project.
- `IP_SECRET_API_KEY` is for sending emails. You can create one for free by visiting https://indiepitcher.com or by replacing injecting `IndiePitcherEmailService` with `MockEmailService` to disable sending emails.
- Set up your local dev environment, you need to spin up a database. An easy way is by downloading [Docker](https://www.docker.com) and typing in following commands
- `docker-compose build`
- `docker-compose up db` starts a local database to develop against
Expand Down
52 changes: 18 additions & 34 deletions Sources/App/Controllers/OrganizationController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -307,23 +307,15 @@ struct OrganizationController: RouteCollection {

await req.trackAnalyticsEvent(name: "organization_member_added", params: ["organization_id": organizationId.uuidString, "member_email": profileToAdd.email, "member_role": update.role.rawValue])

if req.application.environment != .testing {
do {

let emailBody = """
Hi \(profileToAdd.name?.split(separator: " ").first ?? "there"),
This is an automated message to let you know that you've been added to organization \(organization.name) as \(update.role.rawValue) by \(profile.name ?? profile.email).
"""

try await req.indiePitcher.sendEmail(data: .init(to: update.email,
subject: "You've been ivited to \(organization.name)",
body: emailBody,
bodyFormat: .markdown))
} catch {
req.logger.error("\(error)")
}
}
let emailBody = """
Hi \(profileToAdd.name?.split(separator: " ").first ?? "there"),
This is an automated message to let you know that you've been added to organization \(organization.name) as \(update.role.rawValue) by \(profile.name ?? profile.email).
"""

try await req.services.emailService.sendEmail(to: update.email,
subject: "You've been ivited to \(organization.name)",
markdown: emailBody)

return OrganizationMemberDTO(email: update.email, role: update.role, status: .joined)

Expand Down Expand Up @@ -354,23 +346,15 @@ struct OrganizationController: RouteCollection {

await req.trackAnalyticsEvent(name: "organization_member_invitation_created", params: ["organization_id": organizationId.uuidString, "member_email": update.email, "member_role": update.role.rawValue])

if req.application.environment != .testing {
do {

let emailBody = """
Hi there,
This is an automated message to let you know that you've been invited to organization \(organization.name) as \(update.role.rawValue) by \(profile.name ?? profile.email).
"""

try await req.indiePitcher.sendEmail(data: .init(to: update.email,
subject: "You've been ivited to \(organization.name)",
body: emailBody,
bodyFormat: .markdown))
} catch {
req.logger.error("\(error)")
}
}
let emailBody = """
Hi there,
This is an automated message to let you know that you've been invited to organization \(organization.name) as \(update.role.rawValue) by \(profile.name ?? profile.email).
"""

try await req.services.emailService.sendEmail(to: update.email,
subject: "You've been ivited to \(organization.name)",
markdown: emailBody)

return OrganizationMemberDTO(email: update.email, role: update.role, status: .invited)
}
Expand Down
79 changes: 25 additions & 54 deletions Sources/App/Controllers/ProfileController.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import Fluent
import Vapor
import IndiePitcherSwift

typealias MailingListPortalSessionDTO = IndiePitcherSwift.MailingListPortalSession

extension Request {
var profile: Profile {
Expand Down Expand Up @@ -189,19 +186,6 @@ struct ProfileController: RouteCollection {
await req.trackAnalyticsEvent(name: "profile_deleted")
return .noContent
}

@Sendable
func createPortalSession(req: Request) async throws -> MailingListPortalSessionDTO {
let profile = try await req.profile

struct Payload: Content {
var returnURL: URL
}

let payload = try req.content.decode(Payload.self)

return try await req.indiePitcher.createMailingListsPortalSession(contactEmail: profile.email, returnURL: payload.returnURL).data
}
}

private func identifyProfile(profile: Profile, req: Request, isNewProfile: Bool, refreshMixpanelOnly: Bool) async throws {
Expand All @@ -225,25 +209,18 @@ private func identifyProfile(profile: Profile, req: Request, isNewProfile: Bool,

await req.mixpanel.peopleSet(distinctId: profileId.uuidString, request: req, setParams: properties)

if req.application.environment != .testing {
do {

if refreshMixpanelOnly == false {
try await req.indiePitcher.addContact(contact: .init(email: profile.email,
userId: profileId.uuidString,
avatarUrl: profile.avatarUrl,
name: profile.name,
updateIfExists: true,
subscribedToLists: isNewProfile ? ["onboarding", "product_updates"] : nil))
}

if isNewProfile {
try await sendWelcomeOnboardingEmail(req: req, profile: profile)
}

} catch {
req.logger.error("\(error)")
do {

if refreshMixpanelOnly == false {
try await req.services.emailService.syncContact(profile: profile, subscribedToLists: isNewProfile ? ["onboarding", "product_updates"] : nil)
}

if isNewProfile {
await sendWelcomeOnboardingEmail(req: req, profile: profile)
}

} catch {
req.logger.error("\(error)")
}
}

Expand All @@ -252,27 +229,21 @@ private func unidentifyProfile(profile: Profile, req: Request) async throws {
await req.mixpanel.peopleDelete(distinctId: profileId.uuidString)
}

private func sendWelcomeOnboardingEmail(req: Request, profile: Profile) async throws {
if req.application.environment != .testing {
do {

let body = """
Hi {{firstName|default:"there"}},
private func sendWelcomeOnboardingEmail(req: Request, profile: Profile) async {
do {

let body = """
Hi {{firstName|default:"there"}},
Thanks for signing up for Welcome to SaaS Backend Template.
Thanks for signing up for Welcome to SaaS Backend Template.
<br/>
All the best in your startup endeavours.
"""

try await req.indiePitcher.sendEmailToContact(data: .init(contactEmail: profile.email,
subject: "Welcome to SaaS Backend Template!",
body: body,
bodyFormat: .markdown,
list: "onboarding",
delaySeconds: 60*5))
} catch {
req.logger.error("\(error)")
}
<br/>
All the best in your startup endeavours.
"""

try await req.services.emailService.sendPersonalizedEmail(to: profile.email, subject: "Welcome to SaaS Backend Template!", markdown: body, mailingList: "onboarding", delay: 60*5)
} catch {
// don't fail a request just because this has failed
req.logger.error("\(error)")
}
}
Loading

0 comments on commit e69c2fc

Please sign in to comment.