Thêm gói đăng ký Stripe vào ứng dụng web AI của bạn
Kiếm tiền từ ứng dụng web AI của bạn bằng cách thêm gói đăng ký Stripe (theo tháng & theo năm).
Lộ trình & tài nguyên
Yêu cầu cần có (trước khi bắt đầu)
Danh sách điều kiện cần chuẩn bị
Hãy chuẩn bị sẵn những thứ này trước khi bắt đầu.
Trước khi làm theo hướng dẫn Stripe này, hãy đảm bảo ứng dụng web của bạn đã sẵn sàng những thứ sau:
1. Ứng dụng của bạn đã được deploy (bắt buộc có URL live)
Stripe Checkout chỉ hoạt động trên một website đã deploy thật — nó KHÔNG hoạt động bên trong môi trường xem trước của Google AI Studio.
Video hướng dẫn deploy
Xem hướng dẫn deploy
https://youtu.be/HGs-4QjyjHQ?si=EBUOpPx38n9vVROR
---
2. Firebase đã được thiết lập đầy đủ (một video bao trọn mọi thứ)
Phần thiết lập Firebase của bạn phải hoàn tất từ trước, bao gồm: - Đã bật Firebase Authentication - Đã tạo Firestore Database - Ứng dụng web của bạn đã được kết nối với Firebase (bằng cấu hình Firebase của bạn)
Video hướng dẫn thiết lập Firebase
Xem hướng dẫn Firebase
https://youtu.be/juEfJERgBE8?si=je5VF4gKMUJXl8kX
---
✔ Khi những yêu cầu này đã sẵn sàng, bạn có thể tiếp tục từ Bước 1 → Bước 5 mà không gặp trở ngại nào.
Tạo gói đăng ký theo tháng & theo năm
Mở Stripe và chuyển sang Test Mode
Tạo gói đăng ký định kỳ theo tháng
Tạo gói đăng ký định kỳ theo năm
Lưu lại các giá trị Stripe cần thiết để dùng về sau
Kết nối thanh toán Stripe với ứng dụng của bạn
Mở Firebase → Extensions
Cài đặt "Run Payments with Stripe" (do Invertase phát triển)
Dán Stripe Secret Key của bạn vào phần cài đặt của tiện ích
Tạo một Webhook trong Stripe (chọn các sự kiện bắt buộc)
- checkout.session.completed
- customer.subscription.created
- invoice.paid
- invoice.payment_failed
- customer.subscription.updated
- customer.subscription.deleted
- invoice.upcoming
- payment_intent.succeeded
- payment_intent.payment_failed
- customer.subscription.paused
- charge.refunded
- charge.dispute.created
Sao chép Webhook Endpoint URL từ Firebase Functions và dán vào Stripe
Sao chép Signing Secret của Stripe và dán vào Firebase (Webhook Secret)
Lưu lại và xác nhận mọi thứ đã được kết nối
Thiết lập trang Thanh toán thành công & Hủy
Mở dự án của bạn trong Google AI Studio
Chạy Prompt #1: Tạo tệp cấu hình thanh toán (URL đã deploy + Price ID)
Prompt: Tạo tệp cấu hình thanh toán
Tạo một tệp cấu hình thanh toán và điền vào đó các giá trị thật của tôi...
Create a billing config file and fill it with my real values (no placeholders). Use EXACTLY: DEPLOYED_URL = "https://deployment-url.com" MONTHLY_PRICE_ID = "price_MONTHLY_HERE" YEARLY_PRICE_ID = "price_YEARLY_HERE" Create: src/billingConfig.js (or .ts if the project is TypeScript) and export: - DEPLOYED_URL - MONTHLY_PRICE_ID - YEARLY_PRICE_ID - A function successUrl() that returns the correct URL for /success - A function cancelUrl() that returns the correct URL for /failure Important: - Detect if the app uses hash routing automatically: If existing routes contain "/#/" then use DEPLOYED_URL + "/#/success" and "/#/failure" Otherwise use DEPLOYED_URL + "/success" and "/failure" Do not change any existing UI or app logic. Return only the new file content.
Chạy Prompt #2: Tạo trang Success + Cancel
Prompt: Tạo trang Thành công & Hủy
Thêm hai route/trang đơn giản cho các chuyển hướng của Stripe...
Add two simple routes/pages for Stripe redirects: success and failure. Success page: - Text: "Payment successful" - Redirect to dashboard after 1.5 seconds Failure page: - Text: "Payment cancelled" - Redirect to dashboard after 1.5 seconds Use the same routing style already used in the project (hash or normal). Do not redesign UI. Return exact files changed.
Chạy các prompt và xác nhận rằng các tệp/trang đã được thêm vào
Làm cho nút Nâng cấp mở trang thanh toán Stripe
(Tùy chọn) Tạo nút Nâng cấp
Chạy Prompt #3: Thêm modal chọn gói theo tháng/theo năm
Prompt: Thêm giao diện chọn nâng cấp
Thêm giao diện chọn nâng cấp đơn giản mà không thiết kế lại ứng dụng...
Add a simple upgrade choice UI without redesigning the app. When the user clicks the existing Upgrade button: - Show a small modal (or dropdown) with 2 options: 1) Monthly 2) Yearly - Add a close button. Keep current styling. Do not change any other UI. Return exact files changed.
Chạy Prompt #4: Kết nối lựa chọn đó với Stripe Checkout
Prompt: Kết nối thanh toán Stripe
Triển khai thanh toán gói đăng ký Stripe bằng Firebase Stripe Extension...
Implement Stripe subscription checkout using Firebase Stripe Extension. On Monthly selection: - Create a Firestore doc at: customers/{uid}/checkout_sessions/{autoId} - Fields: price: MONTHLY_PRICE_ID (import from billingConfig) mode: "subscription" success_url: successUrl() (import from billingConfig) cancel_url: cancelUrl() (import from billingConfig) On Yearly selection: - Same but price: YEARLY_PRICE_ID After creating the doc: - Listen to that same checkout session document until it contains a field "url" - Redirect the browser to that url Do not add Stripe secret keys to frontend. Do not touch existing CRUD features. Return exact files changed. Test payments using your deployed live URL (not the preview)
Kiểm thử thanh toán bằng URL đã deploy chạy thật của bạn
Chạy Prompt #5: Tự động nâng tài khoản từ Free → Pro
Prompt: Nhận diện gói Pro
Thêm cơ chế nhận diện gói Pro (chỉ đọc) từ Firestore...
Add Pro plan detection (read-only) from Firestore. - Listen in real-time (onSnapshot) to: customers/{uid}/subscriptions - Set isPro = true if any subscription status is "active" or "trialing" - If isPro true: - Show a "Pro" badge - Hide the Upgrade button - If isPro false: - Show a "Free" badge - Show the Upgrade button Important: Do NOT block Free users from using the app features. Do not redesign UI. Return exact files changed.
Chạy Prompt #6: Thêm nút Manage Subscription và kết nối nó với Stripe Billing Portal
Prompt: Nút quản lý gói đăng ký
Nhiệm vụ: Thêm một nút "Manage Subscription" hoạt động được dành cho người dùng PRO...
Task: Add a working "Manage Subscription" button for PRO users using Stripe Customer Portal (Firebase Stripe Extension). Context: - The app already uses the Firebase Stripe Extension (customers/{uid} and checkout_sessions work). - The app uses hash routing (/#/dashboard). - Do NOT change the UI design. Only add logic. - The button must work in production (no CORS errors). Requirements: 1) Show "Manage Subscription" button ONLY when the current user is PRO (isPro === true). 2) On click, DO NOT call any cloudfunctions.net URL directly and DO NOT use fetch to Cloud Functions. 3) Instead, create a Firestore document at: customers/{uid}/portal_sessions/{autoId} with fields: - return_url: "https://vloma.com/#/dashboard" 4) After creating the doc, listen in real-time to that same document until the extension writes a field: - url Then redirect the browser to that url (window.location.href = url). 5) Add basic UX: - disable the button while loading - show a friendly alert if no url appears after 20 seconds - log the real error to console Implementation notes: - Use the existing Firebase client (auth + firestore). - Keep existing files structure. Modify only the minimal files needed. - Return the exact files changed and the exact code changes. Deliverable: - A working Manage Subscription button that opens Stripe Customer Portal and returns to /#/dashboard.
Thêm giới hạn gói Free và Pro (thiết lập SaaS thực tế)
Chạy Prompt #7: Thêm giới hạn giữa gói Free và Pro
Prompt: Thêm giới hạn sử dụng
Nhiệm vụ: Thêm giới hạn sử dụng cho gói Free/Pro mà KHÔNG đụng tới hay thay đổi hệ thống thanh toán/nâng cấp hiện có...
Task: Add Free/Pro usage limits WITHOUT touching or changing the existing billing/upgrade system. CRITICAL RULES (must follow): - DO NOT modify the existing Stripe checkout / upgrade code. - DO NOT modify how "isPro" is determined in the project. - DO NOT create or use users/{uid}.plan field (do not add plan to Firestore at all). - DO NOT change Firestore rules or Storage rules. - Keep UI the same (only small toast/alert/modal allowed). Limits: - FREE: max 5 files, max 5 folders, max 100MB total stored - PRO: max 50 files, max 50 folders, max 5GB total stored How to implement (safe approach): 1) Use the CURRENT existing "isPro" boolean/state already working in the app. - Whatever source it currently uses (Stripe Extension subscriptions / custom claims / existing code), keep it exactly as-is. - Only READ isPro, do not re-implement it. 2) Track usage from the existing Firestore structure (do not rename collections): - Files count = number of docs in users/{uid}/files - Folders count = number of docs in users/{uid}/folders - Total storage used = sum of file sizes from the file docs (field: size bytes). If old docs don’t have size, treat missing size as 0, and ensure new uploads always write size. 3) Enforce limits ONLY in the UI/Client for now: - Before creating a new file doc or uploading: check limits using current isPro. - Before creating a new folder doc: check limits using current isPro. - If Free exceeds limit: block action and show: “Free plan limit reached. Upgrade to Pro to continue.” Add an “Upgrade” button that triggers the EXISTING checkout flow (call the same function already used in the app). 4) IMPORTANT: After checkout completes, the app MUST refresh/reflect Pro automatically: - Do not add new plan logic. - Just ensure the same existing listener/state updates isPro and the UI re-checks limits. - Add a small debug log (console.log) showing current isPro value after returning from checkout. Deliverable: - Limits work and show upgrade prompt. - Paying and upgrading still changes the app to Pro exactly like before. - Return exact files changed.
Thêm Firestore Rules (người dùng chỉ truy cập được dữ liệu của chính mình)
Quan trọng: Các quy tắc bảo mật Firestore này chỉ là ví dụ. Bạn BẮT BUỘC phải xem lại và điều chỉnh chúng cho khớp với cấu trúc dữ liệu của chính ứng dụng web của bạn. Mỗi ứng dụng cần bộ quy tắc phù hợp với collection và mô hình dữ liệu riêng của nó.
firestore.rules
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { function signedIn() { return request.auth != null; } function isOwner(uid) { return signedIn() && request.auth.uid == uid; } // ------------------------- // Users data (your app data) // ------------------------- match /users/{uid} { allow read, write: if isOwner(uid); // Everything under this user is also only for the owner match /{allPaths=**} { allow read, write: if isOwner(uid); } } // ------------------------- // Stripe Extension // customers/{uid}/... // ------------------------- match /customers/{uid} { // user can read their stripe docs allow read: if isOwner(uid); // user must NOT write to stripe docs directly allow write: if false; // user can create a checkout session doc match /checkout_sessions/{id} { allow create, read: if isOwner(uid); allow update, delete: if false; } // user can create a portal session doc match /portal_sessions/{id} { allow create, read: if isOwner(uid); allow update, delete: if false; } // subscriptions/payments are server-controlled (read only) match /subscriptions/{id} { allow read: if isOwner(uid); allow write: if false; } match /payments/{id} { allow read: if isOwner(uid); allow write: if false; } } } }
Thêm Storage Rules (người dùng chỉ truy cập được tệp của chính mình)
Quan trọng: Các quy tắc bảo mật Storage này chỉ là ví dụ. Bạn BẮT BUỘC phải xem lại và điều chỉnh chúng cho khớp với đường dẫn trong dự án của bạn (ví dụ tên thư mục uploads) và các giới hạn của bạn. Mỗi ứng dụng cần bộ quy tắc storage phù hợp với cấu trúc riêng của nó.
storage.rules
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { function signedIn() { return request.auth != null; } function isOwner(uid) { return signedIn() && request.auth.uid == uid; } match /user_uploads/{uid}/{allPaths=**} { allow read: if isOwner(uid); // write create/update/delete // request.resource null delete، allow write: if isOwner(uid) && (request.resource == null || request.resource.size <= 200 * 1024 * 1024); } } }
(Tùy chọn) Chạy một prompt nhanh để sửa các chi tiết giao diện nhỏ (ví dụ: cập nhật giá trong modal)
Sau này bạn có thể chạy thêm một prompt nữa để hoàn thiện các chi tiết giao diện (như cập nhật giá trong modal), nhưng chúng tôi sẽ không đưa ra một prompt cụ thể ở đây vì nó phụ thuộc rất nhiều vào những tùy chỉnh giao diện riêng của bạn.