refactor: implement newsletter module with Resend provider integration
- Introduced a comprehensive newsletter module for managing subscriptions, including functions to subscribe, unsubscribe, and check subscription status. - Created a `ResendNewsletterProvider` class to handle interactions with the Resend API, improving modularity and code organization. - Re-exported relevant types for better accessibility and convenience. - Added a README file detailing usage, configuration, and advanced features of the newsletter module. - Implemented automatic configuration using environment variables and provided a programmatic configuration option.
This commit is contained in:
parent
9e1c648a7c
commit
9c8c54799f
148
src/newsletter/README.md
Normal file
148
src/newsletter/README.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# Newsletter Module
|
||||||
|
|
||||||
|
This module provides functionality for managing newsletter subscriptions using various email service providers. Currently, it supports [Resend](https://resend.com) for handling newsletter subscriptions.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Subscribe users to newsletters
|
||||||
|
- Unsubscribe users from newsletters
|
||||||
|
- Check subscription status
|
||||||
|
- Provider-agnostic interface for easy integration with different newsletter services
|
||||||
|
- Automatic configuration using environment variables
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { subscribe, unsubscribe, isSubscribed } from '@/src/newsletter';
|
||||||
|
|
||||||
|
// Subscribe a user to the newsletter
|
||||||
|
const success = await subscribe('user@example.com');
|
||||||
|
|
||||||
|
// Unsubscribe a user from the newsletter
|
||||||
|
const success = await unsubscribe('user@example.com');
|
||||||
|
|
||||||
|
// Check if a user is subscribed
|
||||||
|
const subscribed = await isSubscribed('user@example.com');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The newsletter module is configured using environment variables:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Required for Resend provider
|
||||||
|
RESEND_API_KEY=your-resend-api-key
|
||||||
|
RESEND_AUDIENCE_ID=your-audience-id
|
||||||
|
```
|
||||||
|
|
||||||
|
Or you can configure it programmatically:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { initializeNewsletterProvider } from '@/src/newsletter';
|
||||||
|
|
||||||
|
// Configure with Resend
|
||||||
|
initializeNewsletterProvider({
|
||||||
|
resend: {
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
audienceId: 'your-audience-id'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Using the Newsletter Provider Directly
|
||||||
|
|
||||||
|
If you need more control, you can interact with the newsletter provider directly:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getNewsletterProvider } from '@/src/newsletter';
|
||||||
|
|
||||||
|
const provider = getNewsletterProvider();
|
||||||
|
|
||||||
|
// Use provider methods directly
|
||||||
|
const result = await provider.subscribe({ email: 'user@example.com' });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating a Provider Instance Manually
|
||||||
|
|
||||||
|
You can create a provider instance directly without configuring the global instance:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createNewsletterProvider, ResendNewsletterProvider } from '@/src/newsletter';
|
||||||
|
|
||||||
|
// Using the factory function
|
||||||
|
const resendProvider = createNewsletterProvider('resend', {
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
audienceId: 'your-audience-id'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Or creating an instance directly
|
||||||
|
const manualProvider = new ResendNewsletterProvider(
|
||||||
|
'your-api-key',
|
||||||
|
'your-audience-id'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using a Custom Provider Implementation
|
||||||
|
|
||||||
|
You can create and use your own newsletter provider implementation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { NewsletterProvider, SubscribeNewsletterProps } from '@/src/newsletter';
|
||||||
|
|
||||||
|
class CustomNewsletterProvider implements NewsletterProvider {
|
||||||
|
async subscribe(params: SubscribeNewsletterProps): Promise<boolean> {
|
||||||
|
// Your implementation
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribe(params: UnsubscribeNewsletterProps): Promise<boolean> {
|
||||||
|
// Your implementation
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkSubscribeStatus(params: CheckSubscribeStatusProps): Promise<boolean> {
|
||||||
|
// Your implementation
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getProviderName(): string {
|
||||||
|
return 'CustomProvider';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use your custom provider directly
|
||||||
|
const customProvider = new CustomNewsletterProvider();
|
||||||
|
const result = await customProvider.subscribe({ email: 'user@example.com' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Main Functions
|
||||||
|
|
||||||
|
- `subscribe(email)`: Subscribe a user to the newsletter
|
||||||
|
- `unsubscribe(email)`: Unsubscribe a user from the newsletter
|
||||||
|
- `isSubscribed(email)`: Check if a user is subscribed to the newsletter
|
||||||
|
|
||||||
|
### Provider Management
|
||||||
|
|
||||||
|
- `getNewsletterProvider()`: Get the configured newsletter provider instance
|
||||||
|
- `initializeNewsletterProvider(config?)`: Initialize the newsletter provider with specific options
|
||||||
|
- `createNewsletterProvider(type, config)`: Create a new provider instance of the specified type
|
||||||
|
|
||||||
|
### Provider Interface
|
||||||
|
|
||||||
|
The `NewsletterProvider` interface defines the following methods:
|
||||||
|
|
||||||
|
- `subscribe(params)`: Subscribe a user to the newsletter
|
||||||
|
- `unsubscribe(params)`: Unsubscribe a user from the newsletter
|
||||||
|
- `checkSubscribeStatus(params)`: Check if a user is subscribed to the newsletter
|
||||||
|
- `getProviderName()`: Get the provider name
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
- `SubscribeNewsletterProps`: Parameters for subscribing a user
|
||||||
|
- `UnsubscribeNewsletterProps`: Parameters for unsubscribing a user
|
||||||
|
- `CheckSubscribeStatusProps`: Parameters for checking subscription status
|
||||||
|
- `NewsletterConfig`: Configuration options for the newsletter module
|
@ -1,4 +1,126 @@
|
|||||||
// export the subscribe and unsubscribe functions
|
import { ResendNewsletterProvider } from './provider/resend';
|
||||||
export { subscribe } from './newsletter';
|
import {
|
||||||
export { unsubscribe } from './newsletter';
|
CheckSubscribeStatusProps,
|
||||||
export { isSubscribed } from './newsletter';
|
NewsletterConfig,
|
||||||
|
NewsletterProvider,
|
||||||
|
SubscribeNewsletterP,
|
||||||
|
UnsubscribeNewsletterProps
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
// Re-export types for convenience
|
||||||
|
export type {
|
||||||
|
NewsletterProvider,
|
||||||
|
NewsletterConfig,
|
||||||
|
SubscribeNewsletterP as SubscribeNewsletterProps,
|
||||||
|
UnsubscribeNewsletterProps,
|
||||||
|
CheckSubscribeStatusProps
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export provider implementation
|
||||||
|
export { ResendNewsletterProvider } from './provider/resend';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global newsletter provider instance
|
||||||
|
*/
|
||||||
|
let newsletterProvider: NewsletterProvider | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the newsletter provider
|
||||||
|
* @returns initialized newsletter provider
|
||||||
|
*/
|
||||||
|
export const initializeNewsletterProvider = (config?: NewsletterConfig): NewsletterProvider => {
|
||||||
|
if (newsletterProvider) {
|
||||||
|
return newsletterProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no config is provided, use environment variables
|
||||||
|
const resendApiKey = process.env.RESEND_API_KEY;
|
||||||
|
const resendAudienceId = process.env.RESEND_AUDIENCE_ID;
|
||||||
|
|
||||||
|
if (config?.resend) {
|
||||||
|
newsletterProvider = new ResendNewsletterProvider(
|
||||||
|
config.resend.apiKey,
|
||||||
|
config.resend.audienceId
|
||||||
|
);
|
||||||
|
} else if (resendApiKey && resendAudienceId) {
|
||||||
|
newsletterProvider = new ResendNewsletterProvider(
|
||||||
|
resendApiKey,
|
||||||
|
resendAudienceId
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Default for development/testing
|
||||||
|
const testApiKey = 'test_api_key';
|
||||||
|
const testAudienceId = 'test_audience_id';
|
||||||
|
newsletterProvider = new ResendNewsletterProvider(testApiKey, testAudienceId);
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'test' && process.env.NODE_ENV !== 'development') {
|
||||||
|
console.warn(
|
||||||
|
'Using Resend with test credentials. This should only happen in development/test environments.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newsletterProvider;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the newsletter provider
|
||||||
|
* @returns current newsletter provider instance
|
||||||
|
*/
|
||||||
|
export const getNewsletterProvider = (): NewsletterProvider => {
|
||||||
|
if (!newsletterProvider) {
|
||||||
|
return initializeNewsletterProvider();
|
||||||
|
}
|
||||||
|
return newsletterProvider;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe a user to the newsletter
|
||||||
|
* @param email The email address to subscribe
|
||||||
|
* @returns True if the subscription was successful, false otherwise
|
||||||
|
*/
|
||||||
|
export const subscribe = async (email: string): Promise<boolean> => {
|
||||||
|
const provider = getNewsletterProvider();
|
||||||
|
return provider.subscribe({ email });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe a user from the newsletter
|
||||||
|
* @param email The email address to unsubscribe
|
||||||
|
* @returns True if the unsubscription was successful, false otherwise
|
||||||
|
*/
|
||||||
|
export const unsubscribe = async (email: string): Promise<boolean> => {
|
||||||
|
const provider = getNewsletterProvider();
|
||||||
|
return provider.unsubscribe({ email });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user is subscribed to the newsletter
|
||||||
|
* @param email The email address to check
|
||||||
|
* @returns True if the user is subscribed, false otherwise
|
||||||
|
*/
|
||||||
|
export const isSubscribed = async (email: string): Promise<boolean> => {
|
||||||
|
const provider = getNewsletterProvider();
|
||||||
|
return provider.checkSubscribeStatus({ email });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a newsletter provider based on the specified type
|
||||||
|
* @param type The provider type
|
||||||
|
* @param config The provider configuration
|
||||||
|
* @returns A configured newsletter provider instance
|
||||||
|
*/
|
||||||
|
export const createNewsletterProvider = (
|
||||||
|
type: string,
|
||||||
|
config: Record<string, any>
|
||||||
|
): NewsletterProvider => {
|
||||||
|
switch (type.toLowerCase()) {
|
||||||
|
case 'resend':
|
||||||
|
return new ResendNewsletterProvider(
|
||||||
|
config.apiKey,
|
||||||
|
config.audienceId
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported newsletter provider type: ${type}`);
|
||||||
|
}
|
||||||
|
};
|
@ -1,16 +0,0 @@
|
|||||||
import { subscribeNewsletter, unsubscribeNewsletter, checkSubscribeStatus } from './provider/resend';
|
|
||||||
|
|
||||||
export const subscribe = async (email: string) => {
|
|
||||||
const subscribed = await subscribeNewsletter({ email });
|
|
||||||
return subscribed;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const unsubscribe = async (email: string) => {
|
|
||||||
const unsubscribed = await unsubscribeNewsletter({ email });
|
|
||||||
return unsubscribed;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isSubscribed = async (email: string) => {
|
|
||||||
const subscribed = await checkSubscribeStatus({ email });
|
|
||||||
return subscribed;
|
|
||||||
};
|
|
@ -1,85 +1,115 @@
|
|||||||
import { CheckSubscribeStatusHandler, SubscribeNewsletterHandler, UnsubscribeNewsletterHandler } from '@/newsletter/types';
|
import { CheckSubscribeStatusParams, NewsletterProvider, SubscribeNewsletterParams, UnsubscribeNewsletterParams } from '@/newsletter/types';
|
||||||
import { Resend } from 'resend';
|
import { Resend } from 'resend';
|
||||||
|
|
||||||
const apiKey = process.env.RESEND_API_KEY || 'test_api_key';
|
|
||||||
const audienceId = process.env.RESEND_AUDIENCE_ID || 'test_audience_id';
|
|
||||||
|
|
||||||
const resend = new Resend(apiKey);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://resend.com/docs/dashboard/audiences/contacts
|
* Implementation of the NewsletterProvider interface using Resend
|
||||||
*/
|
*/
|
||||||
export const subscribeNewsletter: SubscribeNewsletterHandler = async ({ email }) => {
|
export class ResendNewsletterProvider implements NewsletterProvider {
|
||||||
if (!process.env.RESEND_API_KEY || !process.env.RESEND_AUDIENCE_ID) {
|
private resend: Resend;
|
||||||
console.warn('RESEND_API_KEY or RESEND_AUDIENCE_ID not set, skipping subscribe newsletter');
|
private audienceId: string;
|
||||||
return false;
|
|
||||||
|
constructor(apiKey: string, audienceId: string) {
|
||||||
|
this.resend = new Resend(apiKey);
|
||||||
|
this.audienceId = audienceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await resend.contacts.create({
|
getProviderName(): string {
|
||||||
email,
|
return 'Resend';
|
||||||
audienceId,
|
|
||||||
unsubscribed: false,
|
|
||||||
});
|
|
||||||
const subscribed = !result.error;
|
|
||||||
|
|
||||||
if (!subscribed) {
|
|
||||||
console.error('Error subscribing newsletter', result.error);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
console.log('Subscribed newsletter', email);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const unsubscribeNewsletter: UnsubscribeNewsletterHandler = async ({ email }) => {
|
|
||||||
if (!process.env.RESEND_API_KEY || !process.env.RESEND_AUDIENCE_ID) {
|
|
||||||
console.warn('RESEND_API_KEY or RESEND_AUDIENCE_ID not set, skipping unsubscribe newsletter');
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await resend.contacts.update({
|
async subscribe({ email }: SubscribeNewsletterParams): Promise<boolean> {
|
||||||
email,
|
try {
|
||||||
audienceId,
|
// First, list all contacts to find the one with the matching email
|
||||||
unsubscribed: true,
|
const listResult = await this.resend.contacts.list({ audienceId: this.audienceId });
|
||||||
});
|
if (listResult.error) {
|
||||||
const unsubscribed = !result.error;
|
console.error('Error listing contacts:', listResult.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!unsubscribed) {
|
// Check if the contact with the given email exists in the list
|
||||||
console.error('Error unsubscribing newsletter', result.error);
|
let contact = null;
|
||||||
return false;
|
if (listResult.data && Array.isArray(listResult.data)) {
|
||||||
} else {
|
contact = listResult.data.find(c => c.email === email);
|
||||||
console.log('Unsubscribed newsletter', email);
|
}
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const checkSubscribeStatus: CheckSubscribeStatusHandler = async ({ email }) => {
|
// If the contact does not exist, create a new one
|
||||||
if (!process.env.RESEND_API_KEY || !process.env.RESEND_AUDIENCE_ID) {
|
if (!contact) {
|
||||||
console.warn('RESEND_API_KEY or RESEND_AUDIENCE_ID not set, skipping check subscribe status');
|
const createResult = await this.resend.contacts.create({
|
||||||
return false;
|
email,
|
||||||
}
|
audienceId: this.audienceId,
|
||||||
|
unsubscribed: false,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
if (createResult.error) {
|
||||||
// First, list all contacts to find the one with the matching email
|
console.error('Error creating contact', createResult.error);
|
||||||
const listResult = await resend.contacts.list({ audienceId });
|
return false;
|
||||||
|
}
|
||||||
if (listResult.error) {
|
}
|
||||||
console.error('Error listing contacts:', listResult.error);
|
|
||||||
|
// If the contact already exists, update it
|
||||||
|
// NOTICE: we can not just create a new contact if this email already exists,
|
||||||
|
// because Resend will response 201, but user is not subscribed
|
||||||
|
const updateResult = await this.resend.contacts.update({
|
||||||
|
email,
|
||||||
|
audienceId: this.audienceId,
|
||||||
|
unsubscribed: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updateResult.error) {
|
||||||
|
console.error('Error updating contact', updateResult.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Subscribed newsletter', email);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error subscribing newsletter', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the contact with the given email exists in the list
|
|
||||||
// We need to check if data exists and is an array
|
|
||||||
if (listResult.data && Array.isArray(listResult.data)) {
|
|
||||||
// Now we can safely use array methods
|
|
||||||
return listResult.data.some(contact =>
|
|
||||||
contact.email === email && contact.unsubscribed === false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking subscription status:', error);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
async unsubscribe({ email }: UnsubscribeNewsletterParams): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await this.resend.contacts.update({
|
||||||
|
email,
|
||||||
|
audienceId: this.audienceId,
|
||||||
|
unsubscribed: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.error('Error unsubscribing newsletter', result.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Unsubscribed newsletter', email);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error unsubscribing newsletter', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkSubscribeStatus({ email }: CheckSubscribeStatusParams): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// First, list all contacts to find the one with the matching email
|
||||||
|
const listResult = await this.resend.contacts.list({ audienceId: this.audienceId });
|
||||||
|
|
||||||
|
if (listResult.error) {
|
||||||
|
console.error('Error listing contacts:', listResult.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the contact with the given email exists in the list
|
||||||
|
if (listResult.data && Array.isArray(listResult.data)) {
|
||||||
|
return listResult.data.some(contact =>
|
||||||
|
contact.email === email && contact.unsubscribed === false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking subscription status:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
export interface SubscribeNewsletterProps {
|
export interface SubscribeNewsletterParams {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnsubscribeNewsletterProps {
|
export interface UnsubscribeNewsletterParams {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CheckSubscribeStatusProps {
|
export interface CheckSubscribeStatusParams {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SubscribeNewsletterHandler = (params: SubscribeNewsletterProps) => Promise<boolean>;
|
export type SubscribeNewsletterHandler = (params: SubscribeNewsletterParams) => Promise<boolean>;
|
||||||
|
|
||||||
export type UnsubscribeNewsletterHandler = (params: UnsubscribeNewsletterProps) => Promise<boolean>;
|
export type UnsubscribeNewsletterHandler = (params: UnsubscribeNewsletterParams) => Promise<boolean>;
|
||||||
|
|
||||||
export type CheckSubscribeStatusHandler = (params: CheckSubscribeStatusProps) => Promise<boolean>;
|
export type CheckSubscribeStatusHandler = (params: CheckSubscribeStatusParams) => Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Newsletter provider, currently only Resend is supported
|
* Newsletter provider, currently only Resend is supported
|
||||||
@ -23,4 +23,13 @@ export interface NewsletterProvider {
|
|||||||
subscribe: SubscribeNewsletterHandler;
|
subscribe: SubscribeNewsletterHandler;
|
||||||
unsubscribe: UnsubscribeNewsletterHandler;
|
unsubscribe: UnsubscribeNewsletterHandler;
|
||||||
checkSubscribeStatus: CheckSubscribeStatusHandler;
|
checkSubscribeStatus: CheckSubscribeStatusHandler;
|
||||||
|
getProviderName(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NewsletterConfig {
|
||||||
|
provider?: string;
|
||||||
|
resend?: {
|
||||||
|
apiKey: string;
|
||||||
|
audienceId: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user