mirror of
https://github.com/better-auth/better-auth.git
synced 2026-08-22 16:42:53 -05:00
docs: add comprehensive learning guides for payment gateway abstraction
Created four detailed guides to help understand better-auth patterns and apply them to building a payment gateway abstraction library: - LEARNING_GUIDE.md: 17-file study roadmap organized in 5 phases - OAUTH_TO_PAYMENT_MAPPING.md: Mental model showing OAuth-to-Payment parallels - QUICK_START_IMPLEMENTATION.md: Complete implementation guide with code - STUDY_CHECKLIST.md: Interactive checklist to track learning progress These guides show how to learn from better-auth's provider abstraction, plugin system, and configuration patterns to build a "better-payments" library that makes integrating Stripe, Flutterwave, PayStack, and other payment gateways as simple as better-auth makes OAuth providers.
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
# Better-Auth Learning Guide for Payment Gateway Project
|
||||
|
||||
This guide will help you understand the better-auth codebase to build your payment gateway abstraction library.
|
||||
|
||||
## 📚 Learning Path - Study These Files in Order
|
||||
|
||||
### **PHASE 1: Understanding Provider Abstraction** (START HERE)
|
||||
|
||||
#### 1. Provider Interface Definition
|
||||
**File:** `packages/core/src/oauth2/oauth-provider.ts`
|
||||
**Lines to focus on:** 14-83
|
||||
**What to learn:**
|
||||
- How a provider interface is structured
|
||||
- What methods every provider must implement
|
||||
- How TypeScript generics are used for type safety
|
||||
- The difference between required and optional methods
|
||||
|
||||
**Key concepts:**
|
||||
```typescript
|
||||
- OAuthProvider<T, O> interface
|
||||
- createAuthorizationURL() - how providers start flows
|
||||
- validateAuthorizationCode() - how providers verify results
|
||||
- getUserInfo() - how providers fetch data
|
||||
- Optional methods (refreshAccessToken, revokeToken)
|
||||
```
|
||||
|
||||
**Your equivalent:**
|
||||
```typescript
|
||||
PaymentProvider<T, O>
|
||||
- createPaymentIntent()
|
||||
- verifyPayment()
|
||||
- handleWebhook()
|
||||
- Optional: refundPayment(), createSubscription()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2. Simple Provider Implementation
|
||||
**File:** `packages/core/src/social-providers/github.ts`
|
||||
**Lines to focus on:** 1-200
|
||||
**What to learn:**
|
||||
- How to implement the provider interface
|
||||
- How to define provider-specific options
|
||||
- How to make API calls to external services
|
||||
- How to transform external data to standard format
|
||||
|
||||
**Key concepts:**
|
||||
```typescript
|
||||
- GithubProfile interface (provider-specific data)
|
||||
- GithubOptions interface (extends ProviderOptions)
|
||||
- github() factory function
|
||||
- API endpoint configuration
|
||||
- Error handling
|
||||
```
|
||||
|
||||
**Why GitHub first:** Simpler than Google (no OIDC), shows basic patterns clearly
|
||||
|
||||
---
|
||||
|
||||
#### 3. Complex Provider Implementation
|
||||
**File:** `packages/core/src/social-providers/google.ts`
|
||||
**Lines to focus on:** 1-250
|
||||
**What to learn:**
|
||||
- Advanced provider features (ID token verification)
|
||||
- Provider-specific options (accessType, display, hd)
|
||||
- How to handle different authentication flows
|
||||
- Token refresh implementation
|
||||
|
||||
**Key concepts:**
|
||||
```typescript
|
||||
- GoogleProfile interface
|
||||
- GoogleOptions with provider-specific fields
|
||||
- verifyIdToken() for OIDC
|
||||
- refreshAccessToken() implementation
|
||||
- Using helper functions (createAuthorizationURL, validateAuthorizationCode)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 4. Provider with Environment Handling
|
||||
**File:** `packages/core/src/social-providers/paypal.ts`
|
||||
**Lines to focus on:** 1-150
|
||||
**What to learn:**
|
||||
- How to handle sandbox vs production environments
|
||||
- Dynamic endpoint configuration
|
||||
- Environment-based API URLs
|
||||
|
||||
**Key concepts:**
|
||||
```typescript
|
||||
- PayPalOptions with environment field
|
||||
- Conditional endpoint URLs (sandbox vs live)
|
||||
- Environment-aware token endpoints
|
||||
```
|
||||
|
||||
**Why this matters for you:** Payment gateways also have sandbox/production modes!
|
||||
|
||||
---
|
||||
|
||||
#### 5. Provider Registry and Exports
|
||||
**File:** `packages/core/src/social-providers/index.ts`
|
||||
**Lines to focus on:** 1-100
|
||||
**What to learn:**
|
||||
- How to organize multiple providers
|
||||
- Type-safe provider registration
|
||||
- Creating enums from provider names
|
||||
- Configuration type generation
|
||||
|
||||
**Key concepts:**
|
||||
```typescript
|
||||
- socialProviders object (registry)
|
||||
- socialProviderList array
|
||||
- SocialProviders type (for configuration)
|
||||
- Type-safe provider keys
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **PHASE 2: Understanding Configuration System**
|
||||
|
||||
#### 6. Main Configuration Interface
|
||||
**File:** `packages/core/src/types/init-options.ts`
|
||||
**Lines to focus on:** 1-300
|
||||
**What to learn:**
|
||||
- How to structure main configuration
|
||||
- Nested configuration objects
|
||||
- Optional vs required fields
|
||||
- Default values and documentation
|
||||
|
||||
**Key concepts:**
|
||||
```typescript
|
||||
- BetterAuthOptions interface
|
||||
- socialProviders configuration
|
||||
- emailAndPassword configuration
|
||||
- plugins array
|
||||
- advanced options
|
||||
```
|
||||
|
||||
**Your equivalent:**
|
||||
```typescript
|
||||
PaymentGatewayOptions
|
||||
- providers: PaymentProviders
|
||||
- defaultCurrency
|
||||
- webhooks
|
||||
- plugins
|
||||
- advanced
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 7. Provider Options Type
|
||||
**File:** `packages/core/src/oauth2/oauth-provider.ts`
|
||||
**Lines to focus on:** 85-120
|
||||
**What to learn:**
|
||||
- Base options that all providers share
|
||||
- How to allow provider-specific extensions
|
||||
- Generic type parameters for flexibility
|
||||
|
||||
**Key concepts:**
|
||||
```typescript
|
||||
- ProviderOptions<Profile> interface
|
||||
- clientId, clientSecret (common to all)
|
||||
- getUserInfo override option
|
||||
- mapProfileToUser customization
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **PHASE 3: Understanding the Core Factory**
|
||||
|
||||
#### 8. Main Entry Point
|
||||
**File:** `packages/better-auth/src/auth/auth.ts`
|
||||
**Lines to focus on:** 1-50
|
||||
**What to learn:**
|
||||
- How the public API is exposed
|
||||
- Simple wrapper pattern
|
||||
- Type inference from options
|
||||
|
||||
**Key concepts:**
|
||||
```typescript
|
||||
- betterAuth<Options>() function
|
||||
- Generic type parameter
|
||||
- Delegation to createBetterAuth
|
||||
- Return type: Auth<Options>
|
||||
```
|
||||
|
||||
**Your equivalent:**
|
||||
```typescript
|
||||
export const betterPayments = <Options extends PaymentGatewayOptions>(
|
||||
options: Options
|
||||
) => {
|
||||
return createPaymentGateway(options);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 9. Core Factory Function
|
||||
**File:** `packages/better-auth/src/auth/base.ts`
|
||||
**Lines to focus on:** 1-150
|
||||
**What to learn:**
|
||||
- How to initialize the system
|
||||
- Request handler creation
|
||||
- Plugin aggregation
|
||||
- Error code collection
|
||||
|
||||
**Key concepts:**
|
||||
```typescript
|
||||
- createBetterAuth() factory
|
||||
- initFn for async initialization
|
||||
- handler: async (request: Request)
|
||||
- Plugin error code aggregation
|
||||
- Context management
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 10. Context Creation
|
||||
**File:** `packages/better-auth/src/context/create-context.ts`
|
||||
**Lines to focus on:** 1-300
|
||||
**What to learn:**
|
||||
- How to instantiate providers from config
|
||||
- Provider validation
|
||||
- Context object structure
|
||||
|
||||
**Key concepts:**
|
||||
```typescript
|
||||
- Provider instantiation loop
|
||||
- Config validation (checking clientId, clientSecret)
|
||||
- Filtering disabled providers
|
||||
- Creating provider instances from registry
|
||||
```
|
||||
|
||||
**This is where:**
|
||||
```typescript
|
||||
socialProviders: {
|
||||
google: { clientId: "...", clientSecret: "..." }
|
||||
}
|
||||
// Becomes:
|
||||
const provider = socialProviders["google"](config);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **PHASE 4: Understanding Plugin System**
|
||||
|
||||
#### 11. Plugin Interface
|
||||
**File:** `packages/core/src/types/plugin.ts`
|
||||
**Lines to focus on:** 1-200
|
||||
**What to learn:**
|
||||
- Plugin lifecycle hooks
|
||||
- How plugins extend functionality
|
||||
- Database schema definition
|
||||
- Custom endpoints
|
||||
|
||||
**Key concepts:**
|
||||
```typescript
|
||||
- BetterAuthPlugin interface
|
||||
- init() lifecycle hook
|
||||
- endpoints object
|
||||
- schema definition
|
||||
- hooks (before/after)
|
||||
- $ERROR_CODES
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 12. Simple Plugin Example
|
||||
**File:** `packages/better-auth/src/plugins/bearer/index.ts`
|
||||
**Lines to focus on:** 1-200
|
||||
**What to learn:**
|
||||
- How to create a simple plugin
|
||||
- Request/response transformation
|
||||
- Middleware pattern
|
||||
|
||||
**Key concepts:**
|
||||
```typescript
|
||||
- bearer() plugin factory
|
||||
- hooks.before and hooks.after
|
||||
- matcher() function
|
||||
- handler() implementation
|
||||
- Header manipulation
|
||||
```
|
||||
|
||||
**Why bearer:** Simple, focused plugin that shows core patterns
|
||||
|
||||
---
|
||||
|
||||
#### 13. Complex Plugin Example
|
||||
**File:** `packages/stripe/src/index.ts`
|
||||
**Lines to focus on:** 1-500
|
||||
**What to learn:**
|
||||
- How to integrate external services
|
||||
- Database schema in plugins
|
||||
- Custom endpoints
|
||||
- Error handling
|
||||
|
||||
**Key concepts:**
|
||||
```typescript
|
||||
- stripe() plugin factory with options
|
||||
- schema.subscription definition
|
||||
- Multiple endpoints (get-plans, subscribe, cancel)
|
||||
- External API integration (Stripe SDK)
|
||||
- Error codes definition
|
||||
```
|
||||
|
||||
**This is EXACTLY what you're building!** A Stripe plugin but as the core concept.
|
||||
|
||||
---
|
||||
|
||||
#### 14. Plugin Initialization
|
||||
**File:** `packages/better-auth/src/context/helpers.ts`
|
||||
**Lines to focus on:** 1-150 (runPluginInit function)
|
||||
**What to learn:**
|
||||
- How plugins are initialized
|
||||
- How plugins can modify context and options
|
||||
- Plugin composition
|
||||
|
||||
**Key concepts:**
|
||||
```typescript
|
||||
- runPluginInit() function
|
||||
- Iterating through plugins
|
||||
- Calling plugin.init()
|
||||
- Merging options from plugins
|
||||
- Deep merging with defu
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **PHASE 5: Understanding Package Structure**
|
||||
|
||||
#### 15. Monorepo Workspace Config
|
||||
**File:** `pnpm-workspace.yaml`
|
||||
**What to learn:**
|
||||
- How to structure a monorepo
|
||||
- Package organization
|
||||
|
||||
---
|
||||
|
||||
#### 16. Core Package Structure
|
||||
**File:** `packages/core/package.json`
|
||||
**What to learn:**
|
||||
- How to define exports
|
||||
- Dependencies vs peerDependencies
|
||||
- Package naming
|
||||
|
||||
---
|
||||
|
||||
#### 17. Main Package Structure
|
||||
**File:** `packages/better-auth/package.json`
|
||||
**What to learn:**
|
||||
- How to export plugins
|
||||
- Subpath exports pattern
|
||||
- Build configuration
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Reference by Concept
|
||||
|
||||
### **Want to learn:** Provider Abstraction
|
||||
**Read:** Files 1, 2, 3, 4, 5 (in order)
|
||||
|
||||
### **Want to learn:** Configuration System
|
||||
**Read:** Files 6, 7, 10
|
||||
|
||||
### **Want to learn:** Main Entry Point
|
||||
**Read:** Files 8, 9
|
||||
|
||||
### **Want to learn:** Plugin System
|
||||
**Read:** Files 11, 12, 13, 14
|
||||
|
||||
### **Want to learn:** Project Structure
|
||||
**Read:** Files 15, 16, 17
|
||||
|
||||
---
|
||||
|
||||
## 📝 Study Notes Template
|
||||
|
||||
For each file, take notes using this template:
|
||||
|
||||
```markdown
|
||||
### File: [filename]
|
||||
|
||||
**Main Purpose:**
|
||||
[What does this file do?]
|
||||
|
||||
**Key Interfaces/Types:**
|
||||
- [Type name]: [What it represents]
|
||||
- [Type name]: [What it represents]
|
||||
|
||||
**Key Functions:**
|
||||
- [Function name]: [What it does]
|
||||
- [Function name]: [What it does]
|
||||
|
||||
**How I'll use this pattern:**
|
||||
[Your notes on how to apply this to payment gateways]
|
||||
|
||||
**Questions:**
|
||||
[Anything you don't understand]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Practical Exercise
|
||||
|
||||
After studying files 1-5, try this:
|
||||
|
||||
**Exercise 1:** Design your `PaymentProvider` interface
|
||||
- Model it after `OAuthProvider`
|
||||
- Define required methods (create, verify, webhook)
|
||||
- Define optional methods (refund, subscription)
|
||||
|
||||
**Exercise 2:** Implement a Stripe provider
|
||||
- Model it after `google.ts`
|
||||
- Use the Stripe SDK
|
||||
- Follow the same pattern
|
||||
|
||||
**Exercise 3:** Implement a Flutterwave provider
|
||||
- Model it after `github.ts`
|
||||
- Use fetch for API calls
|
||||
- Transform Flutterwave responses to your standard format
|
||||
|
||||
**Exercise 4:** Create a provider registry
|
||||
- Model it after `social-providers/index.ts`
|
||||
- Export all your providers
|
||||
- Create type-safe configuration
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ Estimated Time
|
||||
|
||||
- **Phase 1 (Provider Abstraction):** 3-4 hours
|
||||
- **Phase 2 (Configuration):** 1-2 hours
|
||||
- **Phase 3 (Core Factory):** 2-3 hours
|
||||
- **Phase 4 (Plugin System):** 3-4 hours
|
||||
- **Phase 5 (Package Structure):** 1 hour
|
||||
|
||||
**Total:** ~10-14 hours of focused study
|
||||
|
||||
---
|
||||
|
||||
## 💡 Pro Tips
|
||||
|
||||
1. **Don't read everything at once** - Follow the phases
|
||||
2. **Try implementing as you learn** - Build your version alongside
|
||||
3. **Focus on patterns, not details** - You don't need to understand every line
|
||||
4. **Skip complex parts initially** - Come back to advanced features later
|
||||
5. **Compare OAuth flow to Payment flow** - They're similar:
|
||||
- OAuth: redirect → callback → get token → get user
|
||||
- Payment: create intent → redirect → webhook → verify payment
|
||||
|
||||
---
|
||||
|
||||
## 🎓 What You'll Understand
|
||||
|
||||
After studying these files, you'll know:
|
||||
|
||||
✅ How to create a provider abstraction
|
||||
✅ How to make providers pluggable
|
||||
✅ How to create type-safe configuration
|
||||
✅ How to build a factory function
|
||||
✅ How to create a plugin system
|
||||
✅ How to structure a monorepo
|
||||
✅ How to make your library framework-agnostic
|
||||
|
||||
---
|
||||
|
||||
## 🤔 Common Questions
|
||||
|
||||
**Q: Do I need to understand everything in better-auth?**
|
||||
A: No! Focus on the provider pattern and plugin system. Skip auth-specific logic.
|
||||
|
||||
**Q: Should I copy the code?**
|
||||
A: Copy the **patterns**, not the code. Adapt it for payments.
|
||||
|
||||
**Q: What about the database stuff?**
|
||||
A: Optional for MVP. Start with just payment provider abstraction.
|
||||
|
||||
**Q: How similar should my library be?**
|
||||
A: Very similar in architecture, different in domain (payments vs auth).
|
||||
|
||||
---
|
||||
|
||||
## 📞 Next Steps
|
||||
|
||||
1. Read Phase 1 files (1-5)
|
||||
2. Take notes using the template above
|
||||
3. Try Exercise 1-2
|
||||
4. Share your progress and ask questions!
|
||||
|
||||
Good luck! 🚀
|
||||
@@ -0,0 +1,455 @@
|
||||
# OAuth to Payment Gateway Mapping
|
||||
|
||||
This document shows how OAuth concepts in better-auth map to Payment Gateway concepts in your library.
|
||||
|
||||
## 🔄 Core Concept Mapping
|
||||
|
||||
| OAuth Concept (better-auth) | Payment Concept (your library) | Why It's Similar |
|
||||
|----------------------------|--------------------------------|------------------|
|
||||
| **OAuth Provider** (Google, GitHub) | **Payment Gateway** (Stripe, Flutterwave) | Both are external services with different APIs |
|
||||
| **Authorization URL** | **Payment Intent / Checkout URL** | Both redirect user to external service |
|
||||
| **OAuth Callback** | **Payment Webhook** | Both receive confirmation from external service |
|
||||
| **Access Token** | **Payment ID / Transaction ID** | Both are identifiers for the session/transaction |
|
||||
| **User Profile** | **Payment Details** | Both are data returned from the service |
|
||||
| **Refresh Token** | **Subscription Renewal** | Both handle recurring/long-term access |
|
||||
|
||||
---
|
||||
|
||||
## 📋 Interface Comparison
|
||||
|
||||
### Better-Auth: OAuthProvider Interface
|
||||
|
||||
```typescript
|
||||
export interface OAuthProvider {
|
||||
id: string; // "google", "github"
|
||||
name: string; // "Google", "GitHub"
|
||||
|
||||
// Start the OAuth flow
|
||||
createAuthorizationURL: (data: {
|
||||
state: string;
|
||||
codeVerifier: string;
|
||||
scopes?: string[];
|
||||
redirectURI: string;
|
||||
}) => Promise<URL>;
|
||||
|
||||
// Verify the callback
|
||||
validateAuthorizationCode: (data: {
|
||||
code: string;
|
||||
redirectURI: string;
|
||||
}) => Promise<OAuth2Tokens>;
|
||||
|
||||
// Get user data
|
||||
getUserInfo: (token: OAuth2Tokens) => Promise<{
|
||||
user: OAuth2UserInfo;
|
||||
data: T;
|
||||
}>;
|
||||
|
||||
// Optional: refresh access
|
||||
refreshAccessToken?: (refreshToken: string) => Promise<OAuth2Tokens>;
|
||||
}
|
||||
```
|
||||
|
||||
### Your Library: PaymentProvider Interface
|
||||
|
||||
```typescript
|
||||
export interface PaymentProvider {
|
||||
id: string; // "stripe", "flutterwave"
|
||||
name: string; // "Stripe", "Flutterwave"
|
||||
|
||||
// Start the payment flow
|
||||
createPaymentIntent: (data: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
customerId?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}) => Promise<PaymentIntent>;
|
||||
|
||||
// Verify the payment
|
||||
verifyPayment: (paymentId: string) => Promise<PaymentStatus>;
|
||||
|
||||
// Handle provider callback
|
||||
handleWebhook: (payload: unknown, signature: string) => Promise<WebhookEvent>;
|
||||
|
||||
// Optional: handle refunds
|
||||
refundPayment?: (paymentId: string, amount?: number) => Promise<Refund>;
|
||||
}
|
||||
```
|
||||
|
||||
**See the parallel?** Both follow the same pattern:
|
||||
1. **Initiate** (create URL / create intent)
|
||||
2. **Verify** (validate code / verify payment)
|
||||
3. **Get Data** (get user info / handle webhook)
|
||||
4. **Optional Features** (refresh token / refund)
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Flow Comparison
|
||||
|
||||
### OAuth Flow (better-auth)
|
||||
|
||||
```
|
||||
1. User clicks "Login with Google"
|
||||
↓
|
||||
2. createAuthorizationURL() generates redirect URL
|
||||
↓
|
||||
3. User redirects to Google
|
||||
↓
|
||||
4. User approves on Google's site
|
||||
↓
|
||||
5. Google redirects back with code
|
||||
↓
|
||||
6. validateAuthorizationCode() exchanges code for tokens
|
||||
↓
|
||||
7. getUserInfo() fetches user profile
|
||||
↓
|
||||
8. User is logged in
|
||||
```
|
||||
|
||||
### Payment Flow (your library)
|
||||
|
||||
```
|
||||
1. User clicks "Pay with Stripe"
|
||||
↓
|
||||
2. createPaymentIntent() generates checkout session
|
||||
↓
|
||||
3. User redirects to Stripe checkout
|
||||
↓
|
||||
4. User pays on Stripe's site
|
||||
↓
|
||||
5. Stripe sends webhook to your server
|
||||
↓
|
||||
6. handleWebhook() verifies webhook signature
|
||||
↓
|
||||
7. verifyPayment() confirms payment status
|
||||
↓
|
||||
8. Payment is complete
|
||||
```
|
||||
|
||||
**Same flow, different domain!**
|
||||
|
||||
---
|
||||
|
||||
## 📦 Provider Implementation Comparison
|
||||
|
||||
### Better-Auth: Google Provider
|
||||
|
||||
```typescript
|
||||
export const google = (options: GoogleOptions) => {
|
||||
return {
|
||||
id: "google",
|
||||
name: "Google",
|
||||
|
||||
async createAuthorizationURL({ state, scopes, redirectURI }) {
|
||||
const url = new URL("https://accounts.google.com/o/oauth2/auth");
|
||||
url.searchParams.set("client_id", options.clientId);
|
||||
url.searchParams.set("redirect_uri", redirectURI);
|
||||
url.searchParams.set("scope", scopes.join(" "));
|
||||
url.searchParams.set("state", state);
|
||||
return url;
|
||||
},
|
||||
|
||||
async validateAuthorizationCode({ code, redirectURI }) {
|
||||
const response = await fetch("https://oauth2.googleapis.com/token", {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({
|
||||
code,
|
||||
client_id: options.clientId,
|
||||
client_secret: options.clientSecret,
|
||||
redirect_uri: redirectURI,
|
||||
grant_type: "authorization_code",
|
||||
}),
|
||||
});
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
async getUserInfo(token) {
|
||||
const user = decodeJwt(token.idToken);
|
||||
return {
|
||||
user: {
|
||||
id: user.sub,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
},
|
||||
data: user,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Your Library: Stripe Provider
|
||||
|
||||
```typescript
|
||||
export const stripe = (options: StripeOptions) => {
|
||||
const stripeClient = new Stripe(options.apiKey);
|
||||
|
||||
return {
|
||||
id: "stripe",
|
||||
name: "Stripe",
|
||||
|
||||
async createPaymentIntent({ amount, currency, customerId, metadata }) {
|
||||
const paymentIntent = await stripeClient.paymentIntents.create({
|
||||
amount,
|
||||
currency,
|
||||
customer: customerId,
|
||||
metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
id: paymentIntent.id,
|
||||
clientSecret: paymentIntent.client_secret,
|
||||
amount: paymentIntent.amount,
|
||||
status: paymentIntent.status,
|
||||
};
|
||||
},
|
||||
|
||||
async verifyPayment(paymentId) {
|
||||
const paymentIntent = await stripeClient.paymentIntents.retrieve(paymentId);
|
||||
|
||||
return {
|
||||
id: paymentIntent.id,
|
||||
status: paymentIntent.status,
|
||||
paid: paymentIntent.status === "succeeded",
|
||||
amount: paymentIntent.amount,
|
||||
};
|
||||
},
|
||||
|
||||
async handleWebhook(payload, signature) {
|
||||
const event = stripeClient.webhooks.constructEvent(
|
||||
payload,
|
||||
signature,
|
||||
options.webhookSecret
|
||||
);
|
||||
|
||||
return {
|
||||
type: event.type,
|
||||
data: event.data.object,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Notice:** Same structure, different API calls!
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Plugin Comparison
|
||||
|
||||
### Better-Auth: Stripe Plugin (for subscriptions)
|
||||
|
||||
```typescript
|
||||
export const stripe = (options: StripeOptions) => {
|
||||
return {
|
||||
id: "stripe",
|
||||
|
||||
schema: {
|
||||
subscription: {
|
||||
fields: {
|
||||
stripeSubscriptionId: { type: "string" },
|
||||
stripeCustomerId: { type: "string" },
|
||||
plan: { type: "string" },
|
||||
status: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
endpoints: {
|
||||
"subscription/subscribe": createAuthEndpoint(...),
|
||||
"subscription/cancel": createAuthEndpoint(...),
|
||||
},
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Your Library: Subscription Plugin
|
||||
|
||||
```typescript
|
||||
export const subscriptions = (options: SubscriptionOptions) => {
|
||||
return {
|
||||
id: "subscriptions",
|
||||
|
||||
schema: {
|
||||
subscription: {
|
||||
fields: {
|
||||
paymentProviderId: { type: "string" },
|
||||
subscriptionId: { type: "string" },
|
||||
plan: { type: "string" },
|
||||
status: { type: "string" },
|
||||
currentPeriodEnd: { type: "date" },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
endpoints: {
|
||||
"subscription/create": createPaymentEndpoint(...),
|
||||
"subscription/cancel": createPaymentEndpoint(...),
|
||||
"subscription/upgrade": createPaymentEndpoint(...),
|
||||
},
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Same plugin architecture!**
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration Comparison
|
||||
|
||||
### Better-Auth Configuration
|
||||
|
||||
```typescript
|
||||
const auth = betterAuth({
|
||||
// Configure multiple OAuth providers
|
||||
socialProviders: {
|
||||
google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
scopes: ["email", "profile"],
|
||||
},
|
||||
github: {
|
||||
clientId: process.env.GITHUB_CLIENT_ID,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||
},
|
||||
},
|
||||
|
||||
// Add plugins
|
||||
plugins: [
|
||||
bearer(),
|
||||
organization(),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Your Library Configuration
|
||||
|
||||
```typescript
|
||||
const payments = betterPayments({
|
||||
// Configure multiple payment providers
|
||||
providers: {
|
||||
stripe: {
|
||||
apiKey: process.env.STRIPE_SECRET_KEY,
|
||||
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
},
|
||||
flutterwave: {
|
||||
secretKey: process.env.FLUTTERWAVE_SECRET_KEY,
|
||||
publicKey: process.env.FLUTTERWAVE_PUBLIC_KEY,
|
||||
},
|
||||
},
|
||||
|
||||
// Add plugins
|
||||
plugins: [
|
||||
subscriptions(),
|
||||
invoicing(),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
**Identical configuration pattern!**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Usage Comparison
|
||||
|
||||
### Better-Auth Usage
|
||||
|
||||
```typescript
|
||||
// Redirect to Google login
|
||||
const url = await auth.api.signInSocial({
|
||||
provider: "google",
|
||||
callbackURL: "/auth/callback",
|
||||
});
|
||||
|
||||
// Verify callback
|
||||
const session = await auth.api.verifyCallback({
|
||||
code: request.query.code,
|
||||
});
|
||||
|
||||
// Get user
|
||||
const user = await auth.api.getSession({
|
||||
token: session.token,
|
||||
});
|
||||
```
|
||||
|
||||
### Your Library Usage
|
||||
|
||||
```typescript
|
||||
// Create Stripe payment
|
||||
const intent = await payments.payment.create("stripe", {
|
||||
amount: 5000,
|
||||
currency: "USD",
|
||||
});
|
||||
|
||||
// Redirect user to intent.clientSecret or payment URL
|
||||
|
||||
// Verify webhook
|
||||
const event = await payments.payment.handleWebhook("stripe", {
|
||||
payload: request.body,
|
||||
signature: request.headers["stripe-signature"],
|
||||
});
|
||||
|
||||
// Get payment status
|
||||
const status = await payments.payment.verify("stripe", event.paymentId);
|
||||
```
|
||||
|
||||
**Same API design!**
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Key Takeaways
|
||||
|
||||
| Better-Auth Does | Your Library Does | Same Pattern |
|
||||
|------------------|-------------------|--------------|
|
||||
| Abstracts OAuth providers | Abstracts payment gateways | ✅ Provider abstraction |
|
||||
| Supports Google, GitHub, etc. | Supports Stripe, Flutterwave, etc. | ✅ Multiple providers |
|
||||
| Plugins add features (SSO, passkey) | Plugins add features (subscriptions, invoicing) | ✅ Plugin system |
|
||||
| Type-safe configuration | Type-safe configuration | ✅ TypeScript generics |
|
||||
| Framework agnostic | Framework agnostic | ✅ Works anywhere |
|
||||
| Monorepo structure | Monorepo structure | ✅ Package organization |
|
||||
|
||||
---
|
||||
|
||||
## 💡 Aha Moments
|
||||
|
||||
### 1. **Provider = Service Abstraction**
|
||||
Better-auth abstracts OAuth providers (Google, GitHub)
|
||||
→ You abstract payment providers (Stripe, Flutterwave)
|
||||
|
||||
### 2. **Same Flow, Different Domain**
|
||||
OAuth: redirect → callback → get user
|
||||
→ Payment: checkout → webhook → verify payment
|
||||
|
||||
### 3. **Plugins = Features**
|
||||
Better-auth plugins: SSO, organizations, passkeys
|
||||
→ Your plugins: subscriptions, invoicing, split payments
|
||||
|
||||
### 4. **Configuration = Developer Experience**
|
||||
Better-auth makes OAuth simple
|
||||
→ You make payments simple
|
||||
|
||||
---
|
||||
|
||||
## 📚 Study Strategy
|
||||
|
||||
When reading better-auth code, mentally replace:
|
||||
|
||||
- `OAuthProvider` → `PaymentProvider`
|
||||
- `createAuthorizationURL` → `createPaymentIntent`
|
||||
- `validateAuthorizationCode` → `verifyPayment`
|
||||
- `getUserInfo` → `handleWebhook`
|
||||
- `google()` → `stripe()`
|
||||
- `github()` → `flutterwave()`
|
||||
|
||||
The patterns are **identical**, only the domain changes!
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Next Steps
|
||||
|
||||
1. ✅ Read this mapping document
|
||||
2. ✅ Study the files in LEARNING_GUIDE.md
|
||||
3. ✅ Keep this mental mapping in mind
|
||||
4. ✅ When you see OAuth code, think "How does this apply to payments?"
|
||||
5. ✅ Start implementing your own providers
|
||||
|
||||
**You've got this!** 🚀
|
||||
@@ -0,0 +1,837 @@
|
||||
# Quick Start: Building Your Payment Gateway Library
|
||||
|
||||
After studying better-auth, here's how to start building your payment gateway abstraction library.
|
||||
|
||||
## 🚀 Project Setup
|
||||
|
||||
### Step 1: Create Monorepo Structure
|
||||
|
||||
```bash
|
||||
mkdir better-payments
|
||||
cd better-payments
|
||||
|
||||
# Initialize pnpm workspace
|
||||
pnpm init
|
||||
|
||||
# Create packages
|
||||
mkdir -p packages/core/src/providers
|
||||
mkdir -p packages/core/src/types
|
||||
mkdir -p packages/better-payments/src
|
||||
mkdir -p packages/stripe-plugin/src
|
||||
mkdir -p examples/nextjs
|
||||
```
|
||||
|
||||
### Step 2: Create `pnpm-workspace.yaml`
|
||||
|
||||
```yaml
|
||||
packages:
|
||||
- 'packages/*'
|
||||
- 'examples/*'
|
||||
```
|
||||
|
||||
### Step 3: Create Root `package.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "better-payments-workspace",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "pnpm -r build",
|
||||
"dev": "pnpm -r --parallel dev",
|
||||
"test": "pnpm -r test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.0",
|
||||
"tsup": "^8.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Step-by-Step Implementation
|
||||
|
||||
### Phase 1: Core Types
|
||||
|
||||
**File:** `packages/core/src/types/provider.ts`
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Core payment provider interface
|
||||
* Inspired by better-auth's OAuthProvider
|
||||
*/
|
||||
|
||||
export interface PaymentIntent {
|
||||
id: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
status: 'pending' | 'processing' | 'succeeded' | 'failed' | 'canceled';
|
||||
clientSecret?: string;
|
||||
paymentUrl?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface PaymentStatus {
|
||||
id: string;
|
||||
status: 'pending' | 'processing' | 'succeeded' | 'failed' | 'canceled';
|
||||
amount: number;
|
||||
currency: string;
|
||||
paid: boolean;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface WebhookEvent {
|
||||
type: string;
|
||||
paymentId: string;
|
||||
status: PaymentStatus['status'];
|
||||
data: any;
|
||||
}
|
||||
|
||||
export interface Refund {
|
||||
id: string;
|
||||
paymentId: string;
|
||||
amount: number;
|
||||
status: 'pending' | 'succeeded' | 'failed';
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface CreatePaymentData {
|
||||
amount: number;
|
||||
currency: string;
|
||||
customerId?: string;
|
||||
customerEmail?: string;
|
||||
description?: string;
|
||||
metadata?: Record<string, any>;
|
||||
returnUrl?: string;
|
||||
}
|
||||
|
||||
// Base options that all providers share
|
||||
export interface ProviderOptions<TProfile = any> {
|
||||
// Provider credentials
|
||||
apiKey?: string;
|
||||
secretKey?: string;
|
||||
publicKey?: string;
|
||||
webhookSecret?: string;
|
||||
|
||||
// Environment
|
||||
environment?: 'sandbox' | 'production' | 'test';
|
||||
|
||||
// Customization
|
||||
onPaymentSuccess?: (payment: PaymentStatus) => Promise<void> | void;
|
||||
onPaymentFailure?: (payment: PaymentStatus) => Promise<void> | void;
|
||||
mapPaymentData?: (data: TProfile) => Partial<PaymentStatus>;
|
||||
}
|
||||
|
||||
// Main provider interface
|
||||
export interface PaymentProvider<
|
||||
TProviderData extends Record<string, any> = Record<string, any>,
|
||||
TOptions extends Record<string, any> = Partial<ProviderOptions>
|
||||
> {
|
||||
id: string; // "stripe", "flutterwave", "paystack"
|
||||
name: string; // "Stripe", "Flutterwave", "PayStack"
|
||||
|
||||
// Required methods
|
||||
createPaymentIntent: (data: CreatePaymentData) => Promise<PaymentIntent>;
|
||||
|
||||
verifyPayment: (paymentId: string) => Promise<PaymentStatus>;
|
||||
|
||||
handleWebhook: (payload: any, signature: string) => Promise<WebhookEvent>;
|
||||
|
||||
// Optional methods
|
||||
refundPayment?: (paymentId: string, amount?: number, reason?: string) => Promise<Refund>;
|
||||
|
||||
cancelPayment?: (paymentId: string) => Promise<PaymentStatus>;
|
||||
|
||||
getPayment?: (paymentId: string) => Promise<PaymentStatus>;
|
||||
|
||||
// Configuration
|
||||
options?: TOptions;
|
||||
}
|
||||
|
||||
export type LiteralString = string & {};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: First Provider (Stripe)
|
||||
|
||||
**File:** `packages/core/src/providers/stripe.ts`
|
||||
|
||||
```typescript
|
||||
import Stripe from 'stripe';
|
||||
import type { PaymentProvider, ProviderOptions, CreatePaymentData } from '../types/provider.js';
|
||||
|
||||
export interface StripeProfile {
|
||||
id: string;
|
||||
object: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
status: string;
|
||||
// ... add more Stripe-specific fields
|
||||
}
|
||||
|
||||
export interface StripeOptions extends ProviderOptions<StripeProfile> {
|
||||
apiKey: string;
|
||||
webhookSecret: string;
|
||||
apiVersion?: string;
|
||||
}
|
||||
|
||||
export const stripe = (options: StripeOptions) => {
|
||||
// Initialize Stripe client
|
||||
const stripeClient = new Stripe(options.apiKey, {
|
||||
apiVersion: options.apiVersion || '2023-10-16',
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'stripe' as const,
|
||||
name: 'Stripe',
|
||||
|
||||
async createPaymentIntent(data: CreatePaymentData) {
|
||||
const paymentIntent = await stripeClient.paymentIntents.create({
|
||||
amount: data.amount,
|
||||
currency: data.currency,
|
||||
customer: data.customerId,
|
||||
description: data.description,
|
||||
metadata: data.metadata || {},
|
||||
// Enable automatic payment methods
|
||||
automatic_payment_methods: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: paymentIntent.id,
|
||||
amount: paymentIntent.amount,
|
||||
currency: paymentIntent.currency,
|
||||
status: mapStripeStatus(paymentIntent.status),
|
||||
clientSecret: paymentIntent.client_secret || undefined,
|
||||
metadata: paymentIntent.metadata,
|
||||
};
|
||||
},
|
||||
|
||||
async verifyPayment(paymentId: string) {
|
||||
const paymentIntent = await stripeClient.paymentIntents.retrieve(paymentId);
|
||||
|
||||
const status: PaymentStatus = {
|
||||
id: paymentIntent.id,
|
||||
status: mapStripeStatus(paymentIntent.status),
|
||||
amount: paymentIntent.amount,
|
||||
currency: paymentIntent.currency,
|
||||
paid: paymentIntent.status === 'succeeded',
|
||||
metadata: paymentIntent.metadata,
|
||||
};
|
||||
|
||||
// Call custom handler if provided
|
||||
if (status.paid && options.onPaymentSuccess) {
|
||||
await options.onPaymentSuccess(status);
|
||||
}
|
||||
|
||||
return status;
|
||||
},
|
||||
|
||||
async handleWebhook(payload: any, signature: string) {
|
||||
let event: Stripe.Event;
|
||||
|
||||
try {
|
||||
event = stripeClient.webhooks.constructEvent(
|
||||
payload,
|
||||
signature,
|
||||
options.webhookSecret
|
||||
);
|
||||
} catch (err) {
|
||||
throw new Error(`Webhook signature verification failed: ${err.message}`);
|
||||
}
|
||||
|
||||
const paymentIntent = event.data.object as Stripe.PaymentIntent;
|
||||
|
||||
return {
|
||||
type: event.type,
|
||||
paymentId: paymentIntent.id,
|
||||
status: mapStripeStatus(paymentIntent.status),
|
||||
data: event.data.object,
|
||||
};
|
||||
},
|
||||
|
||||
async refundPayment(paymentId: string, amount?: number, reason?: string) {
|
||||
const refund = await stripeClient.refunds.create({
|
||||
payment_intent: paymentId,
|
||||
amount,
|
||||
reason: reason as Stripe.RefundCreateParams.Reason,
|
||||
});
|
||||
|
||||
return {
|
||||
id: refund.id,
|
||||
paymentId: paymentId,
|
||||
amount: refund.amount,
|
||||
status: refund.status === 'succeeded' ? 'succeeded' :
|
||||
refund.status === 'pending' ? 'pending' : 'failed',
|
||||
reason,
|
||||
};
|
||||
},
|
||||
|
||||
async cancelPayment(paymentId: string) {
|
||||
const paymentIntent = await stripeClient.paymentIntents.cancel(paymentId);
|
||||
|
||||
return {
|
||||
id: paymentIntent.id,
|
||||
status: mapStripeStatus(paymentIntent.status),
|
||||
amount: paymentIntent.amount,
|
||||
currency: paymentIntent.currency,
|
||||
paid: false,
|
||||
};
|
||||
},
|
||||
|
||||
async getPayment(paymentId: string) {
|
||||
return this.verifyPayment(paymentId);
|
||||
},
|
||||
|
||||
options,
|
||||
} satisfies PaymentProvider<StripeProfile, StripeOptions>;
|
||||
};
|
||||
|
||||
// Helper function to map Stripe statuses to our standard statuses
|
||||
function mapStripeStatus(status: string): PaymentStatus['status'] {
|
||||
switch (status) {
|
||||
case 'requires_payment_method':
|
||||
case 'requires_confirmation':
|
||||
case 'requires_action':
|
||||
return 'pending';
|
||||
case 'processing':
|
||||
return 'processing';
|
||||
case 'succeeded':
|
||||
return 'succeeded';
|
||||
case 'canceled':
|
||||
return 'canceled';
|
||||
default:
|
||||
return 'failed';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Second Provider (Flutterwave)
|
||||
|
||||
**File:** `packages/core/src/providers/flutterwave.ts`
|
||||
|
||||
```typescript
|
||||
import type { PaymentProvider, ProviderOptions, CreatePaymentData } from '../types/provider.js';
|
||||
|
||||
export interface FlutterwaveProfile {
|
||||
id: number;
|
||||
tx_ref: string;
|
||||
flw_ref: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
status: string;
|
||||
// ... add more Flutterwave-specific fields
|
||||
}
|
||||
|
||||
export interface FlutterwaveOptions extends ProviderOptions<FlutterwaveProfile> {
|
||||
publicKey: string;
|
||||
secretKey: string;
|
||||
encryptionKey: string;
|
||||
environment?: 'sandbox' | 'production';
|
||||
}
|
||||
|
||||
export const flutterwave = (options: FlutterwaveOptions) => {
|
||||
const baseURL = options.environment === 'production'
|
||||
? 'https://api.flutterwave.com/v3'
|
||||
: 'https://api.flutterwave.com/v3'; // Same for both
|
||||
|
||||
return {
|
||||
id: 'flutterwave' as const,
|
||||
name: 'Flutterwave',
|
||||
|
||||
async createPaymentIntent(data: CreatePaymentData) {
|
||||
const txRef = `tx-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const response = await fetch(`${baseURL}/payments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${options.secretKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tx_ref: txRef,
|
||||
amount: data.amount,
|
||||
currency: data.currency,
|
||||
redirect_url: data.returnUrl,
|
||||
customer: {
|
||||
email: data.customerEmail,
|
||||
...(data.customerId && { customer_id: data.customerId }),
|
||||
},
|
||||
customizations: {
|
||||
title: data.description || 'Payment',
|
||||
},
|
||||
meta: data.metadata,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Flutterwave API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return {
|
||||
id: result.data.id.toString(),
|
||||
amount: data.amount,
|
||||
currency: data.currency,
|
||||
status: 'pending',
|
||||
paymentUrl: result.data.link,
|
||||
metadata: { tx_ref: txRef, ...data.metadata },
|
||||
};
|
||||
},
|
||||
|
||||
async verifyPayment(paymentId: string) {
|
||||
const response = await fetch(
|
||||
`${baseURL}/transactions/${paymentId}/verify`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${options.secretKey}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Flutterwave verification error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const data = result.data;
|
||||
|
||||
const status: PaymentStatus = {
|
||||
id: data.id.toString(),
|
||||
status: mapFlutterwaveStatus(data.status),
|
||||
amount: data.amount,
|
||||
currency: data.currency,
|
||||
paid: data.status === 'successful',
|
||||
metadata: data.meta,
|
||||
};
|
||||
|
||||
if (status.paid && options.onPaymentSuccess) {
|
||||
await options.onPaymentSuccess(status);
|
||||
}
|
||||
|
||||
return status;
|
||||
},
|
||||
|
||||
async handleWebhook(payload: any, signature: string) {
|
||||
// Verify webhook signature
|
||||
const crypto = await import('crypto');
|
||||
const hash = crypto
|
||||
.createHmac('sha256', options.encryptionKey)
|
||||
.update(JSON.stringify(payload))
|
||||
.digest('hex');
|
||||
|
||||
if (hash !== signature) {
|
||||
throw new Error('Invalid webhook signature');
|
||||
}
|
||||
|
||||
return {
|
||||
type: payload.event,
|
||||
paymentId: payload.data.id.toString(),
|
||||
status: mapFlutterwaveStatus(payload.data.status),
|
||||
data: payload.data,
|
||||
};
|
||||
},
|
||||
|
||||
async refundPayment(paymentId: string, amount?: number) {
|
||||
const response = await fetch(`${baseURL}/transactions/${paymentId}/refund`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${options.secretKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
amount,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Flutterwave refund error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return {
|
||||
id: result.data.id.toString(),
|
||||
paymentId,
|
||||
amount: result.data.amount,
|
||||
status: 'succeeded',
|
||||
};
|
||||
},
|
||||
|
||||
options,
|
||||
} satisfies PaymentProvider<FlutterwaveProfile, FlutterwaveOptions>;
|
||||
};
|
||||
|
||||
function mapFlutterwaveStatus(status: string): PaymentStatus['status'] {
|
||||
switch (status) {
|
||||
case 'successful':
|
||||
return 'succeeded';
|
||||
case 'failed':
|
||||
return 'failed';
|
||||
case 'pending':
|
||||
return 'pending';
|
||||
default:
|
||||
return 'processing';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Provider Registry
|
||||
|
||||
**File:** `packages/core/src/providers/index.ts`
|
||||
|
||||
```typescript
|
||||
import { stripe } from './stripe.js';
|
||||
import { flutterwave } from './flutterwave.js';
|
||||
|
||||
export const paymentProviders = {
|
||||
stripe,
|
||||
flutterwave,
|
||||
// Add more providers here
|
||||
} as const;
|
||||
|
||||
export const paymentProviderList = Object.keys(paymentProviders) as [
|
||||
'stripe',
|
||||
...(keyof typeof paymentProviders)[],
|
||||
];
|
||||
|
||||
export type PaymentProviderList = typeof paymentProviderList;
|
||||
|
||||
export type PaymentProviders = {
|
||||
[K in PaymentProviderList[number]]?: Parameters<
|
||||
(typeof paymentProviders)[K]
|
||||
>[0] & {
|
||||
enabled?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
// Re-export providers
|
||||
export { stripe } from './stripe.js';
|
||||
export { flutterwave } from './flutterwave.js';
|
||||
export type { StripeOptions } from './stripe.js';
|
||||
export type { FlutterwaveOptions } from './flutterwave.js';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Main Configuration
|
||||
|
||||
**File:** `packages/core/src/types/options.ts`
|
||||
|
||||
```typescript
|
||||
import type { PaymentProviders } from '../providers/index.js';
|
||||
|
||||
export interface PaymentGatewayOptions {
|
||||
/**
|
||||
* Payment providers configuration
|
||||
*/
|
||||
providers: PaymentProviders;
|
||||
|
||||
/**
|
||||
* Default currency for payments
|
||||
* @default "USD"
|
||||
*/
|
||||
defaultCurrency?: string;
|
||||
|
||||
/**
|
||||
* Webhook configuration
|
||||
*/
|
||||
webhooks?: {
|
||||
/**
|
||||
* Path for webhook endpoint
|
||||
* @default "/api/payments/webhook"
|
||||
*/
|
||||
path?: string;
|
||||
|
||||
/**
|
||||
* Handle webhook events
|
||||
*/
|
||||
onEvent?: (event: WebhookEvent) => Promise<void> | void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugins
|
||||
*/
|
||||
plugins?: PaymentPlugin[];
|
||||
|
||||
/**
|
||||
* Advanced options
|
||||
*/
|
||||
advanced?: {
|
||||
/**
|
||||
* Retry failed payments automatically
|
||||
*/
|
||||
retryFailedPayments?: boolean;
|
||||
|
||||
/**
|
||||
* Auto refund on cancellation
|
||||
*/
|
||||
autoRefundOnCancel?: boolean;
|
||||
|
||||
/**
|
||||
* Enable logging
|
||||
*/
|
||||
logging?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PaymentPlugin {
|
||||
id: string;
|
||||
init?: (context: any) => Promise<void> | void;
|
||||
endpoints?: Record<string, any>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: Main Factory Function
|
||||
|
||||
**File:** `packages/better-payments/src/index.ts`
|
||||
|
||||
```typescript
|
||||
import type { PaymentGatewayOptions } from '@better-payments/core/types/options';
|
||||
import { paymentProviders } from '@better-payments/core/providers';
|
||||
import type { PaymentProvider } from '@better-payments/core/types/provider';
|
||||
|
||||
export const betterPayments = <Options extends PaymentGatewayOptions>(
|
||||
options: Options
|
||||
) => {
|
||||
// Instantiate providers from configuration
|
||||
const providers = Object.entries(options.providers || {})
|
||||
.map(([key, config]) => {
|
||||
if (!config || config.enabled === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providerFactory = paymentProviders[key as keyof typeof paymentProviders];
|
||||
if (!providerFactory) {
|
||||
console.warn(`Provider ${key} not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return providerFactory(config as any);
|
||||
})
|
||||
.filter((p): p is PaymentProvider => p !== null);
|
||||
|
||||
// Helper to find provider
|
||||
const findProvider = (providerId: string) => {
|
||||
const provider = providers.find(p => p.id === providerId);
|
||||
if (!provider) {
|
||||
throw new Error(`Provider ${providerId} not found or not enabled`);
|
||||
}
|
||||
return provider;
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Payment operations
|
||||
*/
|
||||
payment: {
|
||||
/**
|
||||
* Create a new payment
|
||||
*/
|
||||
create: async (providerId: string, data: CreatePaymentData) => {
|
||||
const provider = findProvider(providerId);
|
||||
return provider.createPaymentIntent({
|
||||
...data,
|
||||
currency: data.currency || options.defaultCurrency || 'USD',
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify a payment
|
||||
*/
|
||||
verify: async (providerId: string, paymentId: string) => {
|
||||
const provider = findProvider(providerId);
|
||||
return provider.verifyPayment(paymentId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Refund a payment
|
||||
*/
|
||||
refund: async (providerId: string, paymentId: string, amount?: number, reason?: string) => {
|
||||
const provider = findProvider(providerId);
|
||||
if (!provider.refundPayment) {
|
||||
throw new Error(`Provider ${providerId} does not support refunds`);
|
||||
}
|
||||
return provider.refundPayment(paymentId, amount, reason);
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel a payment
|
||||
*/
|
||||
cancel: async (providerId: string, paymentId: string) => {
|
||||
const provider = findProvider(providerId);
|
||||
if (!provider.cancelPayment) {
|
||||
throw new Error(`Provider ${providerId} does not support cancellation`);
|
||||
}
|
||||
return provider.cancelPayment(paymentId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get payment details
|
||||
*/
|
||||
get: async (providerId: string, paymentId: string) => {
|
||||
const provider = findProvider(providerId);
|
||||
if (provider.getPayment) {
|
||||
return provider.getPayment(paymentId);
|
||||
}
|
||||
return provider.verifyPayment(paymentId);
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* HTTP handler for webhooks
|
||||
*/
|
||||
handler: async (request: Request) => {
|
||||
const url = new URL(request.url);
|
||||
const webhookPath = options.webhooks?.path || '/api/payments/webhook';
|
||||
|
||||
if (!url.pathname.endsWith(webhookPath)) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
// Extract provider from query or header
|
||||
const providerId = url.searchParams.get('provider') ||
|
||||
request.headers.get('x-payment-provider');
|
||||
|
||||
if (!providerId) {
|
||||
return new Response('Provider not specified', { status: 400 });
|
||||
}
|
||||
|
||||
const provider = findProvider(providerId);
|
||||
|
||||
// Get signature from headers
|
||||
const signature = request.headers.get(`${providerId}-signature`) ||
|
||||
request.headers.get('stripe-signature') ||
|
||||
request.headers.get('verif-hash') || '';
|
||||
|
||||
try {
|
||||
const payload = await request.text();
|
||||
const event = await provider.handleWebhook(JSON.parse(payload), signature);
|
||||
|
||||
// Call webhook handler
|
||||
if (options.webhooks?.onEvent) {
|
||||
await options.webhooks.onEvent(event);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ received: true }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Webhook error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: error.message }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Configuration
|
||||
*/
|
||||
options,
|
||||
|
||||
/**
|
||||
* List of enabled providers
|
||||
*/
|
||||
providers: providers.map(p => ({ id: p.id, name: p.name })),
|
||||
};
|
||||
};
|
||||
|
||||
// Re-export types
|
||||
export type { PaymentGatewayOptions } from '@better-payments/core/types/options';
|
||||
export type { PaymentProvider, CreatePaymentData, PaymentIntent, PaymentStatus } from '@better-payments/core/types/provider';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Usage Example
|
||||
|
||||
**File:** `examples/nextjs/app/api/payments/route.ts`
|
||||
|
||||
```typescript
|
||||
import { betterPayments } from 'better-payments';
|
||||
|
||||
const payments = betterPayments({
|
||||
providers: {
|
||||
stripe: {
|
||||
apiKey: process.env.STRIPE_SECRET_KEY!,
|
||||
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
|
||||
onPaymentSuccess: async (payment) => {
|
||||
console.log('Payment succeeded:', payment.id);
|
||||
// Send email, update database, etc.
|
||||
},
|
||||
},
|
||||
flutterwave: {
|
||||
publicKey: process.env.FLUTTERWAVE_PUBLIC_KEY!,
|
||||
secretKey: process.env.FLUTTERWAVE_SECRET_KEY!,
|
||||
encryptionKey: process.env.FLUTTERWAVE_ENCRYPTION_KEY!,
|
||||
},
|
||||
},
|
||||
defaultCurrency: 'USD',
|
||||
});
|
||||
|
||||
// Create payment endpoint
|
||||
export async function POST(request: Request) {
|
||||
const { provider, amount, currency } = await request.json();
|
||||
|
||||
try {
|
||||
const payment = await payments.payment.create(provider, {
|
||||
amount,
|
||||
currency,
|
||||
customerEmail: 'customer@example.com',
|
||||
description: 'Test payment',
|
||||
});
|
||||
|
||||
return Response.json(payment);
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook endpoint
|
||||
export async function POST_WEBHOOK(request: Request) {
|
||||
return payments.handler(request);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ What You've Built
|
||||
|
||||
After following this guide, you have:
|
||||
|
||||
- ✅ Core provider interface
|
||||
- ✅ Stripe provider implementation
|
||||
- ✅ Flutterwave provider implementation
|
||||
- ✅ Provider registry system
|
||||
- ✅ Type-safe configuration
|
||||
- ✅ Main factory function
|
||||
- ✅ Webhook handling
|
||||
- ✅ Framework-agnostic API
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Next Steps
|
||||
|
||||
1. **Add more providers** (PayStack, PayPal, Square)
|
||||
2. **Add plugins** (subscriptions, invoicing, split payments)
|
||||
3. **Add database support** (save payment records)
|
||||
4. **Add React hooks** (usePayment, useSubscription)
|
||||
5. **Add documentation** (README, API reference)
|
||||
6. **Add tests** (unit tests, integration tests)
|
||||
7. **Publish to npm** (better-payments)
|
||||
|
||||
---
|
||||
|
||||
**You've got the foundation! Start building!** 🚀
|
||||
@@ -0,0 +1,206 @@
|
||||
# Better-Auth Study Checklist
|
||||
|
||||
Use this checklist to track your progress as you study the better-auth codebase.
|
||||
|
||||
## 📋 Phase 1: Provider Abstraction
|
||||
|
||||
- [ ] **File 1:** `packages/core/src/oauth2/oauth-provider.ts` (Lines 14-83)
|
||||
- [ ] Understand `OAuthProvider` interface
|
||||
- [ ] Note required methods
|
||||
- [ ] Note optional methods
|
||||
- [ ] Understand generic types `<T, O>`
|
||||
|
||||
- [ ] **File 2:** `packages/core/src/social-providers/github.ts` (Lines 1-200)
|
||||
- [ ] Understand `GithubProfile` interface
|
||||
- [ ] Understand `GithubOptions` interface
|
||||
- [ ] See how `github()` factory works
|
||||
- [ ] Note API endpoint patterns
|
||||
|
||||
- [ ] **File 3:** `packages/core/src/social-providers/google.ts` (Lines 1-250)
|
||||
- [ ] Compare with GitHub provider
|
||||
- [ ] Understand additional features (verifyIdToken)
|
||||
- [ ] Note token refresh pattern
|
||||
- [ ] See helper function usage
|
||||
|
||||
- [ ] **File 4:** `packages/core/src/social-providers/paypal.ts` (Lines 1-150)
|
||||
- [ ] Understand environment handling (sandbox/production)
|
||||
- [ ] Note conditional endpoint URLs
|
||||
- [ ] See how it differs from GitHub/Google
|
||||
|
||||
- [ ] **File 5:** `packages/core/src/social-providers/index.ts` (Lines 1-100)
|
||||
- [ ] Understand `socialProviders` registry
|
||||
- [ ] Understand `socialProviderList` enum
|
||||
- [ ] Understand `SocialProviders` type
|
||||
- [ ] Note how types are generated
|
||||
|
||||
**Exercise after Phase 1:**
|
||||
- [ ] Design your `PaymentProvider` interface (on paper or in code)
|
||||
- [ ] List required methods for payment providers
|
||||
- [ ] List optional methods for payment providers
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 2: Configuration System
|
||||
|
||||
- [ ] **File 6:** `packages/core/src/types/init-options.ts` (Lines 1-300)
|
||||
- [ ] Understand `BetterAuthOptions` structure
|
||||
- [ ] Note how `socialProviders` is configured
|
||||
- [ ] Note how plugins array works
|
||||
- [ ] See `emailAndPassword` config pattern
|
||||
|
||||
- [ ] **File 7:** `packages/core/src/oauth2/oauth-provider.ts` (Lines 85-120)
|
||||
- [ ] Understand `ProviderOptions<Profile>` interface
|
||||
- [ ] Note common fields (clientId, clientSecret)
|
||||
- [ ] Note customization options (getUserInfo, mapProfileToUser)
|
||||
|
||||
**Exercise after Phase 2:**
|
||||
- [ ] Design your `PaymentGatewayOptions` interface
|
||||
- [ ] Design your `ProviderOptions` base interface
|
||||
- [ ] Design provider-specific options (StripeOptions, FlutterwaveOptions)
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 3: Core Factory
|
||||
|
||||
- [ ] **File 8:** `packages/better-auth/src/auth/auth.ts` (Lines 1-50)
|
||||
- [ ] Understand `betterAuth()` function
|
||||
- [ ] Note how generics are used
|
||||
- [ ] See delegation to `createBetterAuth`
|
||||
|
||||
- [ ] **File 9:** `packages/better-auth/src/auth/base.ts` (Lines 1-150)
|
||||
- [ ] Understand `createBetterAuth()` factory
|
||||
- [ ] Note the `handler` function
|
||||
- [ ] See plugin aggregation pattern
|
||||
- [ ] Understand error code collection
|
||||
|
||||
- [ ] **File 10:** `packages/better-auth/src/context/create-context.ts` (Lines 1-300)
|
||||
- [ ] See how providers are instantiated from config
|
||||
- [ ] Note provider validation
|
||||
- [ ] Understand context object structure
|
||||
|
||||
**Exercise after Phase 3:**
|
||||
- [ ] Sketch out your `betterPayments()` function
|
||||
- [ ] Sketch out your `createPaymentGateway()` factory
|
||||
- [ ] Design your context object structure
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 4: Plugin System
|
||||
|
||||
- [ ] **File 11:** `packages/core/src/types/plugin.ts` (Lines 1-200)
|
||||
- [ ] Understand `BetterAuthPlugin` interface
|
||||
- [ ] Note lifecycle hooks (init, before, after)
|
||||
- [ ] Understand schema definition
|
||||
- [ ] Note endpoints pattern
|
||||
|
||||
- [ ] **File 12:** `packages/better-auth/src/plugins/bearer/index.ts` (Lines 1-200)
|
||||
- [ ] Understand bearer plugin pattern
|
||||
- [ ] See hooks.before and hooks.after
|
||||
- [ ] Note matcher function
|
||||
- [ ] Understand header manipulation
|
||||
|
||||
- [ ] **File 13:** `packages/stripe/src/index.ts` (Lines 1-500)
|
||||
- [ ] See complex plugin example
|
||||
- [ ] Note schema definition for subscriptions
|
||||
- [ ] Note multiple endpoints pattern
|
||||
- [ ] See Stripe SDK integration
|
||||
- [ ] Note error codes definition
|
||||
|
||||
- [ ] **File 14:** `packages/better-auth/src/context/helpers.ts` (Lines 1-150)
|
||||
- [ ] Understand `runPluginInit()` function
|
||||
- [ ] See how plugins modify options
|
||||
- [ ] Note deep merge pattern
|
||||
|
||||
**Exercise after Phase 4:**
|
||||
- [ ] Design your `PaymentPlugin` interface
|
||||
- [ ] Sketch a subscriptions plugin
|
||||
- [ ] Sketch an invoicing plugin
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 5: Package Structure
|
||||
|
||||
- [ ] **File 15:** `pnpm-workspace.yaml`
|
||||
- [ ] Understand monorepo structure
|
||||
- [ ] Note package organization
|
||||
|
||||
- [ ] **File 16:** `packages/core/package.json`
|
||||
- [ ] See exports configuration
|
||||
- [ ] Note dependencies structure
|
||||
|
||||
- [ ] **File 17:** `packages/better-auth/package.json`
|
||||
- [ ] See subpath exports
|
||||
- [ ] Note plugin export pattern
|
||||
|
||||
**Exercise after Phase 5:**
|
||||
- [ ] Design your monorepo structure
|
||||
- [ ] Plan your package names
|
||||
- [ ] Decide on export strategy
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Final Exercises
|
||||
|
||||
- [ ] **Exercise 1:** Implement a basic Stripe provider
|
||||
- [ ] Create `StripeOptions` interface
|
||||
- [ ] Create `stripe()` factory function
|
||||
- [ ] Implement `createPaymentIntent()`
|
||||
- [ ] Implement `verifyPayment()`
|
||||
- [ ] Implement `handleWebhook()`
|
||||
|
||||
- [ ] **Exercise 2:** Implement a basic Flutterwave provider
|
||||
- [ ] Create `FlutterwaveOptions` interface
|
||||
- [ ] Create `flutterwave()` factory function
|
||||
- [ ] Implement core methods
|
||||
- [ ] Handle sandbox/production environments
|
||||
|
||||
- [ ] **Exercise 3:** Create provider registry
|
||||
- [ ] Create `paymentProviders` object
|
||||
- [ ] Create `PaymentProviders` type
|
||||
- [ ] Test type safety
|
||||
|
||||
- [ ] **Exercise 4:** Build minimal working version
|
||||
- [ ] Create `betterPayments()` function
|
||||
- [ ] Support 2 providers (Stripe + Flutterwave)
|
||||
- [ ] Test with actual API calls (sandbox mode)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Progress Tracker
|
||||
|
||||
**Phase 1:** ☐☐☐☐☐ (0/5 files)
|
||||
**Phase 2:** ☐☐ (0/2 files)
|
||||
**Phase 3:** ☐☐☐ (0/3 files)
|
||||
**Phase 4:** ☐☐☐☐ (0/4 files)
|
||||
**Phase 5:** ☐☐☐ (0/3 files)
|
||||
|
||||
**Exercises:** ☐☐☐☐ (0/4 completed)
|
||||
|
||||
**Overall Progress:** 0% (0/21 total tasks)
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes Section
|
||||
|
||||
Use this space to write down:
|
||||
- Questions you have
|
||||
- Aha moments
|
||||
- Patterns you notice
|
||||
- Ideas for your implementation
|
||||
|
||||
```
|
||||
[Your notes here]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Completion
|
||||
|
||||
When you check all boxes above, you'll be ready to:
|
||||
✅ Build your payment gateway abstraction library
|
||||
✅ Support multiple payment providers
|
||||
✅ Create a plugin system
|
||||
✅ Structure a professional monorepo
|
||||
✅ Provide excellent TypeScript types
|
||||
|
||||
**Good luck!** 🚀
|
||||
Reference in New Issue
Block a user