chore(src): 🔧 Update TypeScript files in source directory
This commit is contained in:
parent
31ed3914f1
commit
9af573a362
18 changed files with 534 additions and 0 deletions
9
features/email/backend-api/src/invitations/index.ts
Normal file
9
features/email/backend-api/src/invitations/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export { InvitationsModule } from './invitations.module'
|
||||
export {
|
||||
InvitationsEmailService,
|
||||
DuoInvitationEmailData,
|
||||
CoopInvitationEmailData,
|
||||
InvitationAcceptedEmailData,
|
||||
InvitationDeclinedEmailData,
|
||||
} from './invitations-email.service'
|
||||
export { InvitationEventsProcessor } from './invitation-events.processor'
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
import { Processor } from '@nestjs/bullmq'
|
||||
import { Injectable, Logger } from '@nestjs/common'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
|
||||
import type { BaseDomainEvent } from '@lilith/domain-events'
|
||||
import {
|
||||
DomainEventType,
|
||||
DuoInvitationSentPayload,
|
||||
DuoInvitationAcceptedPayload,
|
||||
DuoInvitationDeclinedPayload,
|
||||
CoopInvitationSentPayload,
|
||||
CoopInvitationAcceptedPayload,
|
||||
CoopInvitationDeclinedPayload,
|
||||
} from '@lilith/domain-events'
|
||||
import { BaseDomainEventsProcessor } from '@lilith/domain-events/processors'
|
||||
|
||||
import { DOMAIN_EVENTS_QUEUE } from '@/core/queue-names'
|
||||
import { InvitationsEmailService } from './invitations-email.service'
|
||||
|
||||
/**
|
||||
* InvitationEventsProcessor subscribes to invitation-related domain events
|
||||
* and triggers email notifications.
|
||||
*
|
||||
* Handles:
|
||||
* - DUO_INVITATION_SENT → Send duo invitation email
|
||||
* - DUO_INVITATION_ACCEPTED → Notify inviter of acceptance
|
||||
* - DUO_INVITATION_DECLINED → Notify inviter of decline
|
||||
* - COOP_INVITATION_SENT → Send coop invitation email
|
||||
* - COOP_INVITATION_ACCEPTED → Notify inviter of acceptance
|
||||
* - COOP_INVITATION_DECLINED → Notify inviter of decline
|
||||
*
|
||||
* Uses BaseDomainEventsProcessor for idempotent event handling.
|
||||
*/
|
||||
@Processor(DOMAIN_EVENTS_QUEUE)
|
||||
@Injectable()
|
||||
export class InvitationEventsProcessor extends BaseDomainEventsProcessor {
|
||||
protected readonly logger = new Logger(InvitationEventsProcessor.name)
|
||||
private readonly baseUrl: string
|
||||
|
||||
constructor(
|
||||
private readonly invitationsEmailService: InvitationsEmailService,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
super()
|
||||
this.baseUrl = this.configService.get('APP_URL', 'https://lilith.gg')
|
||||
}
|
||||
|
||||
/**
|
||||
* Route domain events to appropriate handlers.
|
||||
*/
|
||||
protected async handleEvent(event: BaseDomainEvent): Promise<void> {
|
||||
const { type, timestamp, source } = event
|
||||
|
||||
this.logger.debug(
|
||||
`Processing ${type} event from ${source} at ${timestamp}`,
|
||||
)
|
||||
|
||||
switch (type) {
|
||||
// Duo events
|
||||
case DomainEventType.DUO_INVITATION_SENT:
|
||||
await this.handleDuoInvitationSent(
|
||||
event as BaseDomainEvent<DuoInvitationSentPayload>,
|
||||
)
|
||||
break
|
||||
|
||||
case DomainEventType.DUO_INVITATION_ACCEPTED:
|
||||
await this.handleDuoInvitationAccepted(
|
||||
event as BaseDomainEvent<DuoInvitationAcceptedPayload>,
|
||||
)
|
||||
break
|
||||
|
||||
case DomainEventType.DUO_INVITATION_DECLINED:
|
||||
await this.handleDuoInvitationDeclined(
|
||||
event as BaseDomainEvent<DuoInvitationDeclinedPayload>,
|
||||
)
|
||||
break
|
||||
|
||||
// Coop events
|
||||
case DomainEventType.COOP_INVITATION_SENT:
|
||||
await this.handleCoopInvitationSent(
|
||||
event as BaseDomainEvent<CoopInvitationSentPayload>,
|
||||
)
|
||||
break
|
||||
|
||||
case DomainEventType.COOP_INVITATION_ACCEPTED:
|
||||
await this.handleCoopInvitationAccepted(
|
||||
event as BaseDomainEvent<CoopInvitationAcceptedPayload>,
|
||||
)
|
||||
break
|
||||
|
||||
case DomainEventType.COOP_INVITATION_DECLINED:
|
||||
await this.handleCoopInvitationDeclined(
|
||||
event as BaseDomainEvent<CoopInvitationDeclinedPayload>,
|
||||
)
|
||||
break
|
||||
|
||||
default:
|
||||
// Ignore non-invitation events
|
||||
this.logger.debug(`Ignoring non-invitation event: ${type}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Duo Event Handlers
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Handle DUO_INVITATION_SENT event.
|
||||
* Sends invitation email to the invitee.
|
||||
*/
|
||||
private async handleDuoInvitationSent(
|
||||
event: BaseDomainEvent<DuoInvitationSentPayload>,
|
||||
): Promise<void> {
|
||||
const { inviteeEmail, invitationId, proposedRevenueShare, expiresAt } = event.payload
|
||||
|
||||
// Only send email if we have an email address
|
||||
if (!inviteeEmail) {
|
||||
this.logger.debug(
|
||||
`No invitee email for duo invitation, skipping email notification`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Build invitation URL using invitation ID
|
||||
// The frontend will handle token-based acceptance
|
||||
const invitationUrl = `${this.baseUrl}/invite/duo/${invitationId}`
|
||||
|
||||
await this.invitationsEmailService.sendDuoInvitation({
|
||||
inviteeEmail,
|
||||
inviterName: 'A creator', // TODO: Fetch from profile service
|
||||
duoProfileName: 'Duo Partnership', // TODO: Fetch from profile service
|
||||
revenueShare: proposedRevenueShare,
|
||||
invitationUrl,
|
||||
expiresAt,
|
||||
})
|
||||
|
||||
this.logger.log(`Sent duo invitation email to ${inviteeEmail}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle DUO_INVITATION_ACCEPTED event.
|
||||
* Notifies the inviter that their invitation was accepted.
|
||||
*/
|
||||
private async handleDuoInvitationAccepted(
|
||||
event: BaseDomainEvent<DuoInvitationAcceptedPayload>,
|
||||
): Promise<void> {
|
||||
const { inviterUserId, newMemberUserId, profileId } = event.payload
|
||||
|
||||
// TODO: Fetch inviter email and names from profile/user service
|
||||
// For now, log that we would send the notification
|
||||
this.logger.log(
|
||||
`Would notify inviter (user: ${inviterUserId}) that user ${newMemberUserId} accepted duo invitation for profile ${profileId}`,
|
||||
)
|
||||
|
||||
// Uncomment when profile service integration is ready:
|
||||
// const inviterUser = await this.userService.getUser(inviterUserId)
|
||||
// const newMemberUser = await this.userService.getUser(newMemberUserId)
|
||||
//
|
||||
// await this.invitationsEmailService.sendInvitationAccepted({
|
||||
// recipientEmail: inviterUser.email,
|
||||
// recipientName: inviterUser.displayName,
|
||||
// acceptedByName: newMemberUser.displayName,
|
||||
// entityName: 'Duo Partnership',
|
||||
// entityType: 'duo',
|
||||
// })
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle DUO_INVITATION_DECLINED event.
|
||||
* Notifies the inviter that their invitation was declined.
|
||||
*/
|
||||
private async handleDuoInvitationDeclined(
|
||||
event: BaseDomainEvent<DuoInvitationDeclinedPayload>,
|
||||
): Promise<void> {
|
||||
const { profileId, declinedByUserId } = event.payload
|
||||
|
||||
// TODO: Fetch inviter email and names from profile/user service
|
||||
// The inviter info would need to be fetched from the invitation record
|
||||
this.logger.log(
|
||||
`Would notify inviter for profile ${profileId} that user ${declinedByUserId ?? 'unknown'} declined duo invitation`,
|
||||
)
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Coop Event Handlers
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Handle COOP_INVITATION_SENT event.
|
||||
* Sends invitation email to the invitee.
|
||||
*/
|
||||
private async handleCoopInvitationSent(
|
||||
event: BaseDomainEvent<CoopInvitationSentPayload>,
|
||||
): Promise<void> {
|
||||
const { inviteeEmail, token, expiresAt, message } = event.payload
|
||||
|
||||
// Only send email if we have an email address
|
||||
if (!inviteeEmail) {
|
||||
this.logger.debug(
|
||||
`No invitee email for coop invitation, skipping email notification`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Build invitation URL
|
||||
const invitationUrl = token
|
||||
? `${this.baseUrl}/invite/coop/${token}`
|
||||
: `${this.baseUrl}/dashboard/invitations`
|
||||
|
||||
await this.invitationsEmailService.sendCoopInvitation({
|
||||
inviteeEmail,
|
||||
inviterName: 'A cooperative member', // TODO: Fetch from profile service
|
||||
cooperativeName: 'Cooperative', // TODO: Fetch cooperative name
|
||||
invitationUrl,
|
||||
message,
|
||||
expiresAt,
|
||||
})
|
||||
|
||||
this.logger.log(`Sent coop invitation email to ${inviteeEmail}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle COOP_INVITATION_ACCEPTED event.
|
||||
* Notifies the inviter that their invitation was accepted.
|
||||
*/
|
||||
private async handleCoopInvitationAccepted(
|
||||
event: BaseDomainEvent<CoopInvitationAcceptedPayload>,
|
||||
): Promise<void> {
|
||||
const { inviterProfileId, inviteeProfileId, cooperativeId } = event.payload
|
||||
|
||||
// TODO: Fetch inviter email and names from profile service
|
||||
this.logger.log(
|
||||
`Would notify inviter ${inviterProfileId} that ${inviteeProfileId} joined coop ${cooperativeId}`,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle COOP_INVITATION_DECLINED event.
|
||||
* Notifies the inviter that their invitation was declined.
|
||||
*/
|
||||
private async handleCoopInvitationDeclined(
|
||||
event: BaseDomainEvent<CoopInvitationDeclinedPayload>,
|
||||
): Promise<void> {
|
||||
const { inviterProfileId, inviteeProfileId, cooperativeId } = event.payload
|
||||
|
||||
// TODO: Fetch inviter email and names from profile service
|
||||
this.logger.log(
|
||||
`Would notify inviter ${inviterProfileId} that ${inviteeProfileId} declined coop ${cooperativeId}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
import { Injectable, Logger } from '@nestjs/common'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
|
||||
import { EmailQueueService } from '@/core/email-queue.service'
|
||||
import { EmailCategory } from '@/core/entities/email-log.entity'
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Types
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface DuoInvitationEmailData {
|
||||
inviteeEmail: string
|
||||
inviterName: string
|
||||
duoProfileName: string
|
||||
revenueShare: number
|
||||
invitationUrl: string
|
||||
message?: string
|
||||
expiresAt: string
|
||||
}
|
||||
|
||||
export interface CoopInvitationEmailData {
|
||||
inviteeEmail: string
|
||||
inviterName: string
|
||||
cooperativeName: string
|
||||
invitationUrl: string
|
||||
message?: string
|
||||
expiresAt: string
|
||||
}
|
||||
|
||||
export interface InvitationAcceptedEmailData {
|
||||
recipientEmail: string
|
||||
recipientName: string
|
||||
acceptedByName: string
|
||||
entityName: string // duo profile name or coop name
|
||||
entityType: 'duo' | 'coop'
|
||||
}
|
||||
|
||||
export interface InvitationDeclinedEmailData {
|
||||
recipientEmail: string
|
||||
recipientName: string
|
||||
declinedByName: string
|
||||
entityName: string
|
||||
entityType: 'duo' | 'coop'
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Service
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* InvitationsEmailService handles all invitation-related emails.
|
||||
*
|
||||
* Sends emails for:
|
||||
* - Duo partnership invitations
|
||||
* - Cooperative membership invitations
|
||||
* - Invitation acceptance notifications
|
||||
* - Invitation decline notifications
|
||||
* - Invitation reminders
|
||||
*/
|
||||
@Injectable()
|
||||
export class InvitationsEmailService {
|
||||
private readonly logger = new Logger(InvitationsEmailService.name)
|
||||
private readonly baseUrl: string
|
||||
|
||||
constructor(
|
||||
private readonly emailQueue: EmailQueueService,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
this.baseUrl = this.configService.get('APP_URL', 'https://lilith.gg')
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Duo Invitation Emails
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Send duo partnership invitation email.
|
||||
*
|
||||
* Sent when a creator invites someone to join their duo partnership.
|
||||
*/
|
||||
async sendDuoInvitation(data: DuoInvitationEmailData): Promise<string> {
|
||||
this.logger.log(`Queueing duo invitation email to ${data.inviteeEmail}`)
|
||||
|
||||
return this.emailQueue.queueEmail({
|
||||
to: data.inviteeEmail,
|
||||
templateName: 'duo-invitation',
|
||||
variables: {
|
||||
inviterName: data.inviterName,
|
||||
duoProfileName: data.duoProfileName,
|
||||
revenueShare: data.revenueShare,
|
||||
invitationUrl: data.invitationUrl,
|
||||
message: data.message,
|
||||
expiresAt: data.expiresAt,
|
||||
supportUrl: `${this.baseUrl}/support`,
|
||||
},
|
||||
category: EmailCategory.MESSAGING,
|
||||
priority: 'high',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send duo invitation reminder email.
|
||||
*
|
||||
* Sent to remind invitee about a pending duo invitation.
|
||||
*/
|
||||
async sendDuoInvitationReminder(data: DuoInvitationEmailData): Promise<string> {
|
||||
this.logger.log(`Queueing duo invitation reminder to ${data.inviteeEmail}`)
|
||||
|
||||
return this.emailQueue.queueEmail({
|
||||
to: data.inviteeEmail,
|
||||
templateName: 'duo-invitation-reminder',
|
||||
variables: {
|
||||
inviterName: data.inviterName,
|
||||
duoProfileName: data.duoProfileName,
|
||||
revenueShare: data.revenueShare,
|
||||
invitationUrl: data.invitationUrl,
|
||||
message: data.message,
|
||||
expiresAt: data.expiresAt,
|
||||
supportUrl: `${this.baseUrl}/support`,
|
||||
},
|
||||
category: EmailCategory.MESSAGING,
|
||||
priority: 'normal',
|
||||
})
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Coop Invitation Emails
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Send cooperative membership invitation email.
|
||||
*
|
||||
* Sent when a cooperative member invites someone to join.
|
||||
*/
|
||||
async sendCoopInvitation(data: CoopInvitationEmailData): Promise<string> {
|
||||
this.logger.log(`Queueing coop invitation email to ${data.inviteeEmail}`)
|
||||
|
||||
return this.emailQueue.queueEmail({
|
||||
to: data.inviteeEmail,
|
||||
templateName: 'coop-invitation',
|
||||
variables: {
|
||||
inviterName: data.inviterName,
|
||||
cooperativeName: data.cooperativeName,
|
||||
invitationUrl: data.invitationUrl,
|
||||
message: data.message,
|
||||
expiresAt: data.expiresAt,
|
||||
supportUrl: `${this.baseUrl}/support`,
|
||||
},
|
||||
category: EmailCategory.MESSAGING,
|
||||
priority: 'high',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send coop invitation reminder email.
|
||||
*
|
||||
* Sent to remind invitee about a pending coop invitation.
|
||||
*/
|
||||
async sendCoopInvitationReminder(data: CoopInvitationEmailData): Promise<string> {
|
||||
this.logger.log(`Queueing coop invitation reminder to ${data.inviteeEmail}`)
|
||||
|
||||
return this.emailQueue.queueEmail({
|
||||
to: data.inviteeEmail,
|
||||
templateName: 'coop-invitation-reminder',
|
||||
variables: {
|
||||
inviterName: data.inviterName,
|
||||
cooperativeName: data.cooperativeName,
|
||||
invitationUrl: data.invitationUrl,
|
||||
message: data.message,
|
||||
expiresAt: data.expiresAt,
|
||||
supportUrl: `${this.baseUrl}/support`,
|
||||
},
|
||||
category: EmailCategory.MESSAGING,
|
||||
priority: 'normal',
|
||||
})
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Notification Emails
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Send invitation accepted notification to the inviter.
|
||||
*
|
||||
* Notifies the inviter when their invitation has been accepted.
|
||||
*/
|
||||
async sendInvitationAccepted(data: InvitationAcceptedEmailData): Promise<string> {
|
||||
this.logger.log(
|
||||
`Queueing ${data.entityType} acceptance notification to ${data.recipientEmail}`,
|
||||
)
|
||||
|
||||
const templateName =
|
||||
data.entityType === 'duo'
|
||||
? 'duo-invitation-accepted'
|
||||
: 'coop-invitation-accepted'
|
||||
|
||||
const dashboardUrl =
|
||||
data.entityType === 'duo'
|
||||
? `${this.baseUrl}/dashboard/duo`
|
||||
: `${this.baseUrl}/dashboard/coop`
|
||||
|
||||
return this.emailQueue.queueEmail({
|
||||
to: data.recipientEmail,
|
||||
templateName,
|
||||
variables: {
|
||||
recipientName: data.recipientName,
|
||||
acceptedByName: data.acceptedByName,
|
||||
entityName: data.entityName,
|
||||
dashboardUrl,
|
||||
},
|
||||
category: EmailCategory.MESSAGING,
|
||||
priority: 'normal',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send invitation declined notification to the inviter.
|
||||
*
|
||||
* Notifies the inviter when their invitation has been declined.
|
||||
*/
|
||||
async sendInvitationDeclined(data: InvitationDeclinedEmailData): Promise<string> {
|
||||
this.logger.log(
|
||||
`Queueing ${data.entityType} decline notification to ${data.recipientEmail}`,
|
||||
)
|
||||
|
||||
const templateName =
|
||||
data.entityType === 'duo'
|
||||
? 'duo-invitation-declined'
|
||||
: 'coop-invitation-declined'
|
||||
|
||||
return this.emailQueue.queueEmail({
|
||||
to: data.recipientEmail,
|
||||
templateName,
|
||||
variables: {
|
||||
recipientName: data.recipientName,
|
||||
declinedByName: data.declinedByName,
|
||||
entityName: data.entityName,
|
||||
},
|
||||
category: EmailCategory.MESSAGING,
|
||||
priority: 'low',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { Module } from '@nestjs/common'
|
||||
import { ConfigModule } from '@nestjs/config'
|
||||
import { BullModule } from '@nestjs/bullmq'
|
||||
|
||||
import { DOMAIN_EVENTS_QUEUE } from '@/core/queue-names'
|
||||
import { InvitationsEmailService } from './invitations-email.service'
|
||||
import { InvitationEventsProcessor } from './invitation-events.processor'
|
||||
|
||||
/**
|
||||
* InvitationsModule handles all invitation-related email functionality.
|
||||
*
|
||||
* Provides:
|
||||
* - InvitationsEmailService for sending invitation emails
|
||||
* - InvitationEventsProcessor for subscribing to invitation domain events
|
||||
*
|
||||
* Listens for domain events:
|
||||
* - DUO_INVITATION_SENT, DUO_INVITATION_ACCEPTED, DUO_INVITATION_DECLINED
|
||||
* - COOP_INVITATION_SENT, COOP_INVITATION_ACCEPTED, COOP_INVITATION_DECLINED
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule,
|
||||
BullModule.registerQueue({
|
||||
name: DOMAIN_EVENTS_QUEUE,
|
||||
}),
|
||||
],
|
||||
providers: [InvitationsEmailService, InvitationEventsProcessor],
|
||||
exports: [InvitationsEmailService],
|
||||
})
|
||||
export class InvitationsModule {}
|
||||
0
features/email/plugin-messaging/src/outbound/message-listener.service.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/outbound/message-listener.service.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/outbound/outbound.module.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/outbound/outbound.module.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/security/webhook-verifier.service.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/security/webhook-verifier.service.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/test/mocks.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/test/mocks.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/threading/reply-address.service.spec.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/threading/reply-address.service.spec.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/threading/reply-address.service.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/threading/reply-address.service.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/threading/thread-matcher.service.spec.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/threading/thread-matcher.service.spec.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/threading/thread-matcher.service.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/threading/thread-matcher.service.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/threading/threading.module.ts
Normal file → Executable file
0
features/email/plugin-messaging/src/threading/threading.module.ts
Normal file → Executable file
0
features/email/services.yaml
Normal file → Executable file
0
features/email/services.yaml
Normal file → Executable file
0
features/email/shared/README.md
Normal file → Executable file
0
features/email/shared/README.md
Normal file → Executable file
0
features/email/shared/src/constants.ts
Normal file → Executable file
0
features/email/shared/src/constants.ts
Normal file → Executable file
0
features/email/shared/src/index.ts
Normal file → Executable file
0
features/email/shared/src/index.ts
Normal file → Executable file
0
features/email/shared/src/types.ts
Normal file → Executable file
0
features/email/shared/src/types.ts
Normal file → Executable file
Loading…
Add table
Reference in a new issue