Next.js x Stripeã®å®è£
ã®æµããã·ãã¥ã¬ãŒã·ã§ã³ãã©ãã«ãããããããããã£ãã®ã§ã·ã§ã¢ç¬ð
ïŒïŒïŒïŒ
ãNext.js x Stripeå®è£
ã·ãã¥ã¬ãŒã·ã§ã³ãã©ãã
ã·ãŒã³1: ã³ã¯ãŒãã³ã°ã¹ããŒã¹ã«ãŠ
[æããã³ã¯ãŒãã³ã°ã¹ããŒã¹ãçªéã®åžã§äœæ¥äžã®ããããããé£ã®åžã§ããæ¯ãã€ãå±±ç°ãŠãŠ]
ãŠãŠ: (é ãæ±ããªãã) ããããããŸããšã©ãŒåºã...Reacté£ããã...ã
ããããã: (é£ã®ç»é¢ãã¡ãã£ãšèŠãŠ) ãã©ãããïŒNext.jsã®ã»ããã¢ããã§èºããŠãïŒã
ãŠãŠ: (é©ããŠ) ããã£ïŒïŒãããªãã§ãïŒãã®...ããªãã¯...ïŒã
ããããã: (ã«ãããã«) ããããã£ãŠèšããŸããæ®æ®µã¯ãšã³ãžãã¢ãã£ãŠãŠãããŸã«è¬åž«ãããŠãã3人ã®åã©ãã®ããã§ããããã
ãŠãŠ: ãå±±ç°ãŠãŠã§ãïŒSIerãèŸããŠç¬ç«ããããšããŠããã§ãã...ããã³ããšã³ãéçºãå
šç¶ããããªããŠ...ã
ããããã: ããžããç¬ç«ããäœãäœããããã®ããã®ïŒã
ãŠãŠ: (ç®ãèŒãããŠ) ãã¯ãïŒãPromptBaseãã£ãŠããAIããã³ããã®ã·ã§ã¢ãµãŒãã¹ãäœããããã§ããè¯ãããã³ããã¯ææã«ããŠãäœè
ãåçåã§ããä»çµã¿ã«ããããŠ...ã
ããããã: (èå³ã瀺ããŠ) ããã£ããããããããïŒãããStripeã§ã®æ±ºæžãå¿
èŠã ããã¡ããã©åŸæåéã ããæäŒãããïŒã
ãŠãŠ: (ç®ãäžžãããŠ) ãæ¬åœã§ããïŒïŒãã²ãé¡ãããŸãïŒã
ã·ãŒã³2: ãããžã§ã¯ãã®å§å
[2人ãããŒãã«ãå²ãã§ããŒãPCãéããŠãã]
ããããã: ããŸãã¯ç°å¢æ§ç¯ããã ããNext.jsãããžã§ã¯ãäœæãããã£ãŠã¿ããã
npx create-next-app@latest prompt-base
ãŠãŠ: ãNext.jsã£ãŠæ®éã®Reactãšäœãéããã§ããïŒã
ããããã: ãç°¡åã«èšããšããµãŒããŒãµã€ãã¬ã³ããªã³ã°ãAPIã«ãŒãããã¡ã€ã«ããŒã¹ã®ã«ãŒãã£ã³ã°ãªã©ãæšæºæèŒãããŠããç¹ã«ãµãã¹ã¯ãªãã·ã§ã³æ©èœãäœãæã¯ãAPIã«ãŒãã䟿å©ã ãã
ãŠãŠ: (ã¡ã¢ãåããªãã) ããªãã»ã©...ã
ããããã: ãæ¬¡ã«å¿
èŠãªããã±ãŒãžãã€ã³ã¹ããŒã«ãããã
cd prompt-base
npm install mongoose next-auth @mui/material @mui/icons-material @emotion/react @emotion/styled stripe @stripe/stripe-js
ãŠãŠ: ãçµæ§ãããããããã§ãã...ã
ããããã: (ç¬ããªãã) ãããã ããã§ãåããã±ãŒãžã«ã¯åœ¹å²ãããããmongooseã¯MongoDBã®æäœãnext-authã¯èªèšŒãMUIã¯UIã³ã³ããŒãã³ããstripeã¯æ±ºæžåŠçããããããéèŠãªããŒã¹ãªãã ã
ã·ãŒã³3: ããŒã¿ããŒã¹æ¥ç¶
ããããã: ããŸãã¯MongoDBã®æ¥ç¶èšå®ãããããlib/mongodbãã©ã«ããäœã£ãŠããã®äžã«connection.jsãäœæãããã
// lib/mongodb/connection.js
import mongoose from 'mongoose';
const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
throw new Error('MongoDBã®URIãèšå®ãããŠããŸããã.envãã¡ã€ã«ã確èªããŠãã ããã');
}
// ãã£ãã·ã¥å€æ°
let cached = global.mongoose;
if (!cached) {
cached = global.mongoose = { conn: null, promise: null };
}
async function connectDB() {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = {
bufferCommands: false,
};
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
return mongoose;
});
}
cached.conn = await cached.promise;
return cached.conn;
}
export default connectDB;
ãŠãŠ: (ã³ãŒããèŠãªãã) ããã®cachedã£ãŠå€æ°ã¯äœã®ããã«ãããã§ããïŒã
ããããã: ããã質åã ãïŒNext.jsã¯éçºäžã«ããããªããŒããèµ°ã£ãŠãã³ãŒããé »ç¹ã«åå®è¡ããããã ããã®ãã³ã«MongoDBãžã®æ¥ç¶ãäœãçŽããšéå¹çã ãããäžåºŠäœã£ãæ¥ç¶ãåå©çšããããã®ãã£ãã·ã¥æ©æ§ãªãã ãã
ãŠãŠ: ããªãã»ã©ïŒå¹çåã®å·¥å€«ãªãã§ããã
ããããã: ããããããæ¬¡ã¯.envãã¡ã€ã«ãäœã£ãŠç°å¢å€æ°ãèšå®ãããã
.envãã¡ã€ã«ã®äœæ
MONGODB_URI=mongodb srv://username:password@cluster0.example.mongodb.net/promptbase?retryWrites=true&w=majority
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your_random_string_here
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key
STRIPE_PUBLISHABLE_KEY=pk_test_your_stripe_publishable_key
STRIPE_WEBHOOK_SECRET=whsec_your_stripe_webhook_secret
ãŠãŠ: ãããå
šéšã©ãã§ååŸãããã§ããïŒã
ããããã: (ã«ã£ãã) ãäžã€ãã€ååŸããŠããããããŸãã¯MongoDBã®ã¢ã«ãŠã³ããäœã£ãŠ...ã
ã·ãŒã³4: ã¢ãã«èšèš
[æéãçµéããç°å¢å€æ°ã®èšå®ãå®äºããŠãã]
ããããã: ãæ¬¡ã¯ããŒã¿ã¢ãã«ãäœããããŸãããŠãŒã¶ãŒã¢ãã«ããã
// models/User.js
import mongoose from 'mongoose';
const UserSchema = new mongoose.Schema({
name: String,
email: {
type: String,
required: true,
unique: true,
},
image: String,
stripeCustomerId: String,
subscription: {
status: {
type: String,
enum: ['active', 'canceled', 'past_due', 'none'],
default: 'none'
},
priceId: String,
currentPeriodEnd: Date
},
createdPrompts: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Prompt'
}],
savedPrompts: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Prompt'
}]
}, { timestamps: true });
export default mongoose.models.User || mongoose.model('User', UserSchema);
ããããã: ãããã§ãã€ã³ãã¯ãStripeãšã®é£æºã®ããã®ãã£ãŒã«ããçšæããŠããããšã ããstripeCustomerIdãšãsubscriptionãªããžã§ã¯ãã¯ç¹ã«éèŠã
ãŠãŠ: ããªãã»ã©ïŒãŠããšã¯ãµãã¹ã¯ã®ç¶æ
ããã®subscriptionãªããžã§ã¯ãã§ç®¡çãããã§ããã
ããããã: ãããããïŒæ¬¡ã«ããã³ããã¢ãã«ãäœããã
// models/Prompt.js
import mongoose from 'mongoose';
const PromptSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
description: String,
content: {
type: String,
required: true
},
tags: [String],
creator: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
isPremium: {
type: Boolean,
default: false
},
useCount: {
type: Number,
default: 0
},
likes: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}]
}, { timestamps: true });
export default mongoose.models.Prompt || mongoose.model('Prompt', PromptSchema);
ãŠãŠ: ãisPremiumã£ãŠãã£ãŒã«ãããããã§ããããããææã³ã³ãã³ããå€å¥ãããã§ããïŒã
ããããã: ããã®ãšããïŒãã®ãã©ã°ã§ææããã³ãããã©ããã倿ãããææããã³ããã¯ãµãã¹ã¯äŒå¡ã ããèŠãããããã«ãããã
ã·ãŒã³5: Stripe飿ºã®åºæ¬ã»ããã¢ãã
ããããã: ãæ¬¡ã¯ãStripeãšã®é£æºã®åºç€ãäœãããlib/stripeãã©ã«ããäœæããŠããã®äžã«index.jsãã¡ã€ã«ãäœããã
// lib/stripe/index.js
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2023-10-16', // ææ°ã®API Versionãæå®
});
// æéãã©ã³ID
export const PREMIUM_PRICE_ID = 'price_1234567890'; // å®éã®Stripeããã·ã¥ããŒãã§äœæããPriceã®IDãèšå®
ãŠãŠ: ããã®PREMIUM_PRICE_IDã£ãŠã©ãããååŸãããã§ããïŒã
ããããã: ãStripeã®ããã·ã¥ããŒãããProducts & Pricesã®ã»ã¯ã·ã§ã³ã§ãµãã¹ã¯ãªãã·ã§ã³ãã©ã³ãäœæãããšããã®IDãçºè¡ãããããããšã§Stripeã®ããã·ã¥ããŒããèŠãããã
[æ°ãããã¡ã€ã«ãäœæ]
// lib/stripe/client.js
import { loadStripe } from '@stripe/stripe-js';
let stripePromise;
export const getStripe = () => {
if (!stripePromise) {
stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
}
return stripePromise;
};
ããããã: ããã¡ãã¯ãã©ãŠã¶åŽïŒã¯ã©ã€ã¢ã³ããµã€ãïŒã§Stripeã䜿ãããã®èšå®ã ããloadStripeã¯ãã©ãŠã¶ã§Stripeãåæåãã颿°ã§ããããã·ã³ã°ã«ãã³ãã¿ãŒã³ã§äžåºŠã ãåæåããããã«ããŠããã ã
ãŠãŠ: ããªãã»ã©ããµãŒããŒåŽãšã¯ã©ã€ã¢ã³ãåŽã§å¥ã
ã«èšå®ãå¿
èŠãªãã§ããã
ããããã: ããããªãã ããStripeã¯äž¡æ¹ã§äœ¿ããããããããã§èšå®ãå¿
èŠã«ãªãã
ã·ãŒã³6: NextAuth.jsã§ã®èªèšŒèšå®
ããããã: ãæ¬¡ã¯èªèšŒæ©èœãå®è£
ããããpages/api/auth/[...nextauth].jsãäœæãããã
// pages/api/auth/[...nextauth].js
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import connectDB from '../../../lib/mongodb/connection';
import User from '../../../models/User';
import { stripe } from '../../../lib/stripe';
export default NextAuth({
providers: [
GoogleProvider({
clientId:
process.env.GOOGLE_CLIENT_ID,
clientSecret:
process.env.GOOGLE_CLIENT_SECRET,
}),
],
callbacks: {
async session({ session, token }) {
if (session.user) {
session.user.id = token.sub;
// ããŒã¿ããŒã¹ãããŠãŒã¶ãŒæ
å ±ãååŸ
await connectDB();
const user = await User.findOne({ email:
session.user.email });
if (user) {
session.user.subscription = user.subscription;
session.user.stripeCustomerId = user.stripeCustomerId;
}
}
return session;
},
async signIn({ user, account }) {
await connectDB();
// ãŠãŒã¶ãŒã®ååšç¢ºèª
const userExists = await User.findOne({ email:
user.email });
// æ°èŠãŠãŒã¶ãŒã®å Žå
if (!userExists) {
// Stripeã«ã¹ã¿ããŒäœæ
const customer = await stripe.customers.create({
email:
user.email,
name:
user.name,
});
// ãŠãŒã¶ãŒäœæ
await User.create({
name:
user.name,
email:
user.email,
image: user.image,
stripeCustomerId:
customer.id,
subscription: {
status: 'none',
priceId: null,
currentPeriodEnd: null
}
});
}
return true;
},
},
pages: {
signIn: '/auth/signin',
},
secret: process.env.NEXTAUTH_SECRET,
});
ãŠãŠ: ããããïŒãµã€ã³ã€ã³ãããšãã«èªåã§Stripeã®é¡§å®¢IDãäœæããŠããã§ããã
ããããã: ãããã ããããããã€ã³ããªãã ãæ°èŠãŠãŒã¶ãŒãç»é²ããããåæã«Stripeã®ã«ã¹ã¿ããŒãäœæããŠããã®IDãMongoDBã«ä¿åããŠãããããããããšã§ãåŸã§ãµãã¹ã¯ãªãã·ã§ã³ãäœããšãã«äŸ¿å©ã«ãªããã ã
ãŠãŠ: ããªãã»ã©ïŒæåããStripeãšé£æºããŠãããã§ããã
ã·ãŒã³7: ãµãã¹ã¯ãªãã·ã§ã³è³Œå
¥ããŒãžã®äœæ
ããããã: ãæ¬¡ã¯ããµãã¹ã¯ãªãã·ã§ã³ã賌å
¥ããããã®ããŒãžãäœãããpages/pricingãã©ã«ããäœã£ãŠããã®äžã«index.jsãäœæãããã
// pages/pricing/index.js
import { useState } from 'react';
import { useSession } from 'next-auth/react';
import { Button, Card, CardContent, Typography, Container, Grid, Box } from '@mui/material';
import CheckIcon from '@mui/icons-material/Check';
import { getStripe } from '../../lib/stripe/client';
export default function PricingPage() {
const { data: session } = useSession();
const [isLoading, setIsLoading] = useState(false);
const features = [
'ç¡å¶éã®ããã³ããä¿å',
'ãã¬ãã¢ã ããã³ãããžã®ã¢ã¯ã»ã¹',
'AIãã£ããã§ã®ããã³ããçææ¯æŽ',
'åªå
ãµããŒã'
];
const handleSubscribe = async () => {
if (!session) {
// ãŠãŒã¶ãŒããã°ã€ã³ããŠããªãå Žåã¯ãã°ã€ã³ããŒãžãžãªãã€ã¬ã¯ã
window.location.href = '/auth/signin';
return;
}
setIsLoading(true);
try {
// ãã§ãã¯ã¢ãŠãã»ãã·ã§ã³ãäœæããAPIãåŒã³åºã
const response = await fetch('/api/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
price: 'monthly', // ãŸã㯠'yearly'
}),
});
const { sessionId } = await response.json();
// Stripeãã§ãã¯ã¢ãŠãã«ãªãã€ã¬ã¯ã
const stripe = await getStripe();
stripe.redirectToCheckout({ sessionId });
} catch (error) {
console.error('Error:', error);
alert('ãµãã¹ã¯ãªãã·ã§ã³ã®åŠçäžã«ãšã©ãŒãçºçããŸããã');
} finally {
setIsLoading(false);
}
};
const isSubscribed = session?.user?.subscription?.status === 'active';
return (
<Container maxWidth="md" sx={{ py: 8 }}>
<Typography variant="h3" component="h1" align="center" gutterBottom>
ãã¬ãã¢ã ãã©ã³
</Typography>
<Typography variant="h6" align="center" color="textSecondary" paragraph>
æé«ã®ããã³ããã䜿ã£ãŠãAIã®å¯èœæ§ãæå€§éã«åŒãåºãã
</Typography>
<Box sx={{ mt: 6 }}>
<Grid container justifyContent="center">
<Grid item xs={12} md={6}>
<Card raised sx={{ p: 2 }}>
<CardContent>
<Typography variant="h4" component="h2" align="center" gutterBottom>
ãã¬ãã¢ã ãã©ã³
</Typography>
<Typography variant="h3" component="p" align="center" sx={{ fontWeight: 'bold', my: 2 }}>
Â¥980 <Typography variant="caption">/ æ</Typography>
</Typography>
<Box sx={{ my: 4 }}>
{
features.map((feature, index) => (
<Box key={index} sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<CheckIcon color="primary" sx={{ mr: 1 }} />
<Typography>{feature}</Typography>
</Box>
))}
</Box>
<Button
variant="contained"
color="primary"
size="large"
fullWidth
onClick={handleSubscribe}
disabled={isLoading || isSubscribed}
>
{isSubscribed
? 'ç»é²æžã¿'
: isLoading
? 'åŠçäž...'
: 'ä»ããç»é²ãã'
}
</Button>
{isSubscribed && (
<Typography variant="body2" color="primary" align="center" sx={{ mt: 2 }}>
ãã§ã«ãã¬ãã¢ã ãã©ã³ã«ç»é²ãããŠããŸã
</Typography>
)}
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
</Container>
);
}
ãŠãŠ: ãMUIã®ã³ã³ããŒãã³ãã§ãã£ãããUIãã§ããŸããïŒã§ãããã®/api/checkoutãšã³ããã€ã³ãã¯ãŸã äœã£ãŠãªãã§ãããïŒã
ããããã: ãéããïŒæ¬¡ã¯ãã®APIãšã³ããã€ã³ããäœããã
ã·ãŒã³8: ãã§ãã¯ã¢ãŠãAPIã®å®è£
ããããã: ãpages/api/checkout.jsãäœæãããã
// pages/api/checkout.js
import { getSession } from 'next-auth/react';
import { stripe, PREMIUM_PRICE_ID } from '../../lib/stripe';
import connectDB from '../../lib/mongodb/connection';
import User from '../../models/User';
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method Not Allowed' });
}
try {
// ã»ãã·ã§ã³ãããŠãŒã¶ãŒæ
å ±ãååŸ
const session = await getSession({ req });
if (!session) {
return res.status(401).json({ error: 'Unauthorized' });
}
// MongoDBãããŠãŒã¶ãŒããŒã¿ãååŸ
await connectDB();
const user = await User.findOne({ email:
session.user.email });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Stripeãã§ãã¯ã¢ãŠãã»ãã·ã§ã³ãäœæ
const checkoutSession = await stripe.checkout.sessions.create({
customer: user.stripeCustomerId,
mode: 'subscription',
payment_method_types: ['card'],
line_items: [
{
price: PREMIUM_PRICE_ID,
quantity: 1,
},
],
success_url: `${process.env.NEXTAUTH_URL}/pricing?success=true&session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXTAUTH_URL}/pricing?canceled=true`,
metadata: {
userId: user._id.toString(),
},
});
res.status(200).json({ sessionId:
checkoutSession.id });
} catch (error) {
console.error('Checkout error:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
}
ãŠãŠ: ããªãã»ã©ïŒãŠãŒã¶ãŒããµãã¹ã¯ã«ç»é²ãããšãã®APIã§ãããã§ããããã ãã ãšãŠãŒã¶ãŒãStripeã§æ¯æããå®äºããåŸãããŒã¿ããŒã¹ã®ãµãã¹ã¯æ
å ±ã¯ã©ããã£ãп޿°ããããã§ããïŒã
ããããã: (ã«ã£ãã) ããã質åã ãïŒããã«ã¯Stripeã®Webhookãå¿
èŠãªãã ãStripeã§ã€ãã³ããçºçãããšãèšå®ãããšã³ããã€ã³ãã«ãã®æ
å ±ãéãããŠãããããã䜿ã£ãŠDBãæŽæ°ãããã ãã
ã·ãŒã³9: Webhookå®è£
ããããã: ãpages/api/webhooks/stripe.jsãäœæãããã
// pages/api/webhooks/stripe.js
import { buffer } from 'micro';
import { stripe } from '../../../lib/stripe';
import connectDB from '../../../lib/mongodb/connection';
import User from '../../../models/User';
// Next.jsã®çµã¿èŸŒã¿ã®body parserãç¡å¹å
export const config = {
api: {
bodyParser: false,
},
};
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).end();
}
const buf = await buffer(req);
const sig = req.headers['stripe-signature'];
let event;
try {
// Webhookã®çœ²åãæ€èšŒ
event = stripe.webhooks.constructEvent(
buf.toString(),
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error(`Webhook Error: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// ã€ãã³ãã¿ã€ãã«å¿ããåŠç
try {
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutSessionCompleted(
event.data.object);
break;
case 'invoice.payment_succeeded':
await handleInvoicePaymentSucceeded(
event.data.object);
break;
case 'customer.subscription.updated':
await handleSubscriptionUpdated(
event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionDeleted(
event.data.object);
break;
}
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook handler error:', error);
res.status(500).end();
}
}
// æ°èŠãµãã¹ã¯ãªãã·ã§ã³äœææã®åŠç
async function handleCheckoutSessionCompleted(session) {
if (session.mode !== 'subscription') return;
const subscription = await stripe.subscriptions.retrieve(session.subscription);
const userId = session.metadata.userId;
await connectDB();
// ãŠãŒã¶ãŒã®ãµãã¹ã¯ãªãã·ã§ã³æ
å ±ãæŽæ°
await User.findByIdAndUpdate(userId, {
'subscription.status': 'active',
'subscription.priceId':
subscription.items.data[0].price.id,
'subscription.currentPeriodEnd': new Date(subscription.current_period_end * 1000),
});
}
// ç¶ç¶èª²éæåæã®åŠç
async function handleInvoicePaymentSucceeded(invoice) {
if (invoice.billing_reason !== 'subscription_cycle') return;
const subscription = await stripe.subscriptions.retrieve(invoice.subscription);
await connectDB();
// ã«ã¹ã¿ããŒIDã§ãŠãŒã¶ãŒãæ€çŽ¢ãããµãã¹ã¯ãªãã·ã§ã³æ
å ±ãæŽæ°
await User.findOneAndUpdate(
{ stripeCustomerId: invoice.customer },
{
'subscription.status': 'active',
'subscription.priceId':
subscription.items.data[0].price.id,
'subscription.currentPeriodEnd': new Date(subscription.current_period_end * 1000),
}
);
}
// ãµãã¹ã¯ãªãã·ã§ã³æŽæ°æã®åŠç
async function handleSubscriptionUpdated(subscription) {
await connectDB();
await User.findOneAndUpdate(
{ stripeCustomerId: subscription.customer },
{
'subscription.status': subscription.status,
'subscription.priceId':
subscription.items.data[0].price.id,
'subscription.currentPeriodEnd': new Date(subscription.current_period_end * 1000),
}
);
}
// ãµãã¹ã¯ãªãã·ã§ã³å逿ã®åŠç
async function handleSubscriptionDeleted(subscription) {
await connectDB();
await User.findOneAndUpdate(
{ stripeCustomerId: subscription.customer },
{
'subscription.status': 'canceled',
'subscription.priceId': null,
'subscription.currentPeriodEnd': null,
}
);
}
ãŠãŠ: (é©ãã衚æ
ã§) ããããïŒããã§æ¯æãåŸã®åŠçãèªååããããã§ããïŒã§ãããã®Webhookã£ãŠã©ããã£ãŠãã¹ããããã§ããïŒã
ããããã: ãStripeã«ã¯stripe-cliãšããããŒã«ããã£ãŠãããã䜿ããšããŒã«ã«ç°å¢ã§ãWebhookããã¹ãã§ãããã ããããªæãã§ã³ãã³ããå®è¡ãããã
stripe listen --forward-to localhost:3000/api/webhooks/stripe
ããããã: ããããå®è¡ãããšãStripeããã®ã€ãã³ããããŒã«ã«ç°å¢ã«è»¢éããŠããããã ãããšã§äžç·ã«ãã¹ãããŠã¿ããã
ã·ãŒã³10: ããã³ãã衚瀺ãšãã¬ãã¢ã ã³ã³ãã³ãã®å¶é
ããããã: ãæåŸã«ããã¬ãã¢ã ãŠãŒã¶ãŒã ããèŠãããããã³ããã®å¶éãå®è£
ãããããŸãã¯ããã³ããäžèЧã衚瀺ããAPIãäœããã
// pages/api/prompts/index.js
import connectDB from '../../../lib/mongodb/connection';
import Prompt from '../../../models/Prompt';
import { getSession } from 'next-auth/react';
export default async function handler(req, res) {
await connectDB();
if (req.method === 'GET') {
try {
const session = await getSession({ req });
const isSubscribed = session?.user?.subscription?.status === 'active';
// ã¯ãšãªãã©ã¡ãŒã¿ããæ¡ä»¶ãååŸ
const { limit = 10, page = 1, tag } = req.query;
const skip = (parseInt(page) - 1) * parseInt(limit);
let query = {};
// ã¿ã°ã«ãããã£ã«ã¿ãªã³ã°
if (tag) {
query.tags = tag;
}
// ãµãã¹ã¯ããŠããªããŠãŒã¶ãŒã«ã¯ç¡æããã³ããã®ã¿è¡šç€º
if (!isSubscribed) {
query.isPremium = {
$ne: true };
}
const prompts = await Prompt.find(query)
.populate('creator', 'name image')
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit));
const total = await Prompt.countDocuments(query);
res.status(200).json({
prompts,
total,
pages: Math.ceil(total / parseInt(limit)),
currentPage: parseInt(page)
});
} catch (error) {
console.error('Error fetching prompts:', error);
res.status(500).json({ error: 'Failed to fetch prompts' });
}
} else {
res.status(405).json({ error: 'Method not allowed' });
}
}
ãŠãŠ: ããªãã»ã©ïŒãµãã¹ã¯ãªãã·ã§ã³ã®ç¶æ
ã«å¿ããŠã衚瀺ããããã³ããããã£ã«ã¿ãªã³ã°ããŠããã§ããïŒã
ããããã: ãããïŒæ¬¡ã«ãããã³ããšã³ãã§ããã衚瀺ããã³ã³ããŒãã³ããäœããã
// components/PromptCard.js
import { Card, CardContent, Typography, Chip, Box, Avatar, Button } from '@mui/material';
import LockIcon from '@mui/icons-material/Lock';
import { useSession } from 'next-auth/react';
import Link from 'next/link';
export default function PromptCard({ prompt }) {
const { data: session } = useSession();
const isSubscribed = session?.user?.subscription?.status === 'active';
return (
<Card sx={{ mb: 2, position: 'relative' }}>
{prompt.isPremium && (
<Chip
icon={<LockIcon />}
label="Premium"
color="secondary"
size="small"
sx={{ position: 'absolute', top: 10, right: 10 }}
/>
)}
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Avatar src={prompt.creator.image} sx={{ mr: 2 }} />
<Typography variant="subtitle1">{
prompt.creator.name}</Typography>
</Box>
<Typography variant="h6" gutterBottom>{prompt.title}</Typography>
<Typography variant="body2" color="textSecondary" paragraph>
{prompt.description}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 2 }}>
{
prompt.tags.map((tag, index) => (
<Chip key={index} label={tag} size="small" />
))}
</Box>
{prompt.isPremium && !isSubscribed ? (
<Box sx={{ textAlign: 'center', py: 2 }}>
<Typography variant="body2" color="textSecondary" gutterBottom>
ãã®ãã¬ãã¢ã ããã³ãããèŠãã«ã¯ããµãã¹ã¯ãªãã·ã§ã³ãå¿
èŠã§ã
</Typography>
<Button
component={Link}
href="/pricing"
variant="contained"
color="primary"
size="small"
sx={{ mt: 1 }}
>
ãã¬ãã¢ã ã«ç»é²ãã
</Button>
</Box>
) : (
<Typography variant="body1" sx={{
p: 2,
bgcolor: 'background.paper',
borderRadius: 1,
fontFamily: 'monospace'
}}>
{prompt.isPremium && !isSubscribed
? '***********'
: prompt.content}
</Typography>
)}
</CardContent>
</Card>
);
}
ãŠãŠ: ãããã§ããã¬ãã¢ã ãŠãŒã¶ãŒã ãããã¬ãã¢ã ããã³ãããèŠãããããã«ãªããŸãããïŒã
ããããã: ãããã ãïŒããã§MVPãšããŠã®åºæ¬æ©èœã¯æã£ããããµã€ã³ã¢ãããèªèšŒããµãã¹ã¯ç»é²ããã¬ãã¢ã ã³ã³ãã³ãã®è¡šç€ºå¶éãŸã§äžéãå®è£
ã§ãããã
ãŠãŠ: (è奮æ°å³ã«) ãããããããæ¬åœã«ããããšãããããŸãïŒãããªã«çæéã§ãµãã¹ã¯æ©èœãŸã§å®è£
ã§ãããªããŠæã£ãŠãã¿ãŸããã§ããïŒã
ããããã: (ç¬é¡ã§) ãããã°ã©ãã³ã°ã¯ç©ã¿éãã ããä»ååŠãã ããšãããŒã¹ã«ãå°ããã€æ©èœã远å ããŠããã°ãããã ãããšãå¿ãã¡ããããªãã®ã¯ãå®éã«éçšãããšãã¯Stripeã®æ¬çªç°å¢ã®èšå®ãã»ãã¥ãªãã£å¯Ÿçããã£ããããããšã ãã
ãŠãŠ: ãã¯ãïŒé 匵ããŸãïŒãããããã¿ããã«3人ãåè²ãŠããªãããšã³ãžãã¢ãšããŠæŽ»èºãããã§ãïŒã
ããããã: (ç
§ããªãã) ããŸããåã©ããã¡ã®ãããã§å¹ççã«åãããšãèŠããããéãããæéã§ææãåºãèšç·Žã«ãªããã ãåãèªåã®ããŒã¹ã§é 匵ã£ãŠïŒã
[2人ãããŒãPCãéããŠç· ããããå Žé¢]
çµãã