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
|
||||
export { subscribe } from './newsletter';
|
||||
export { unsubscribe } from './newsletter';
|
||||
export { isSubscribed } from './newsletter';
|
||||
import { ResendNewsletterProvider } from './provider/resend';
|
||||
import {
|
||||
CheckSubscribeStatusProps,
|
||||
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';
|
||||
|
||||
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 }) => {
|
||||
if (!process.env.RESEND_API_KEY || !process.env.RESEND_AUDIENCE_ID) {
|
||||
console.warn('RESEND_API_KEY or RESEND_AUDIENCE_ID not set, skipping subscribe newsletter');
|
||||
return false;
|
||||
export class ResendNewsletterProvider implements NewsletterProvider {
|
||||
private resend: Resend;
|
||||
private audienceId: string;
|
||||
|
||||
constructor(apiKey: string, audienceId: string) {
|
||||
this.resend = new Resend(apiKey);
|
||||
this.audienceId = audienceId;
|
||||
}
|
||||
|
||||
const result = await resend.contacts.create({
|
||||
email,
|
||||
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;
|
||||
getProviderName(): string {
|
||||
return 'Resend';
|
||||
}
|
||||
|
||||
const result = await resend.contacts.update({
|
||||
email,
|
||||
audienceId,
|
||||
unsubscribed: true,
|
||||
});
|
||||
const unsubscribed = !result.error;
|
||||
async subscribe({ email }: SubscribeNewsletterParams): 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;
|
||||
}
|
||||
|
||||
if (!unsubscribed) {
|
||||
console.error('Error unsubscribing newsletter', result.error);
|
||||
return false;
|
||||
} else {
|
||||
console.log('Unsubscribed newsletter', email);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
// Check if the contact with the given email exists in the list
|
||||
let contact = null;
|
||||
if (listResult.data && Array.isArray(listResult.data)) {
|
||||
contact = listResult.data.find(c => c.email === email);
|
||||
}
|
||||
|
||||
export const checkSubscribeStatus: CheckSubscribeStatusHandler = 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 check subscribe status');
|
||||
return false;
|
||||
}
|
||||
// If the contact does not exist, create a new one
|
||||
if (!contact) {
|
||||
const createResult = await this.resend.contacts.create({
|
||||
email,
|
||||
audienceId: this.audienceId,
|
||||
unsubscribed: false,
|
||||
});
|
||||
|
||||
try {
|
||||
// First, list all contacts to find the one with the matching email
|
||||
const listResult = await resend.contacts.list({ audienceId });
|
||||
|
||||
if (listResult.error) {
|
||||
console.error('Error listing contacts:', listResult.error);
|
||||
if (createResult.error) {
|
||||
console.error('Error creating contact', createResult.error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
export interface UnsubscribeNewsletterProps {
|
||||
export interface UnsubscribeNewsletterParams {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface CheckSubscribeStatusProps {
|
||||
export interface CheckSubscribeStatusParams {
|
||||
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
|
||||
@ -23,4 +23,13 @@ export interface NewsletterProvider {
|
||||
subscribe: SubscribeNewsletterHandler;
|
||||
unsubscribe: UnsubscribeNewsletterHandler;
|
||||
checkSubscribeStatus: CheckSubscribeStatusHandler;
|
||||
getProviderName(): string;
|
||||
}
|
||||
|
||||
export interface NewsletterConfig {
|
||||
provider?: string;
|
||||
resend?: {
|
||||
apiKey: string;
|
||||
audienceId: string;
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user