diff --git a/package.json b/package.json
index 82226f019..a1abff867 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,8 @@
},
"dependencies": {
"@gtm-support/vue-gtm": "^2.0.0",
+ "@stripe/stripe-js": "^4.10.0",
+ "@types/stripe": "^8.0.417",
"@vuestic/compiler": "latest",
"@vuestic/tailwind": "^0.1.3",
"@vueuse/core": "^10.6.1",
@@ -35,10 +37,12 @@
"register-service-worker": "^1.7.1",
"sass": "^1.69.5",
"serve": "^14.2.1",
+ "stripe": "^17.3.1",
"vue": "3.5.8",
"vue-chartjs": "^5.3.0",
"vue-i18n": "^9.6.2",
"vue-router": "^4.2.5",
+ "vue-stripe-js": "^1.0.3",
"vuestic-ui": "^1.10.2"
},
"devDependencies": {
diff --git a/src/components/stripe/StripeCardItem.vue b/src/components/stripe/StripeCardItem.vue
new file mode 100644
index 000000000..7b0719e86
--- /dev/null
+++ b/src/components/stripe/StripeCardItem.vue
@@ -0,0 +1,38 @@
+
+
+
+
{{ billingDetails?.name || 'Unnamed Card' }}
+
+
+
+
{{ card.display_brand }} **** {{ card.last4 }}
+
Expires {{ card.exp_month }}/{{ card.exp_year }}
+
+
+
Added on {{ creationDate }}
+
+
+
+
+
+
+
+
diff --git a/src/components/stripe/StripeCardList.vue b/src/components/stripe/StripeCardList.vue
new file mode 100644
index 000000000..06aa0d7f4
--- /dev/null
+++ b/src/components/stripe/StripeCardList.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
diff --git a/src/components/stripe/StripePaymentModal.vue b/src/components/stripe/StripePaymentModal.vue
new file mode 100644
index 000000000..813333c5a
--- /dev/null
+++ b/src/components/stripe/StripePaymentModal.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+ {{ paymentProcessing && payingCardId == card.id ? 'Processing...' : 'Pay' }}
+
+
+
+
+
+
+
diff --git a/src/composables/useStripe.js b/src/composables/useStripe.js
new file mode 100644
index 000000000..81a1d9786
--- /dev/null
+++ b/src/composables/useStripe.js
@@ -0,0 +1,29 @@
+import { onBeforeMount, ref } from 'vue'
+import { loadStripe } from '@stripe/stripe-js'
+
+import { STRIPE_PUBLIC_KEY } from '../config/stripe'
+
+const stripeLoaded = ref(false)
+const stripeInstance = ref(null) // Store the actual Stripe instance
+
+export const useStripe = () => {
+ onBeforeMount(async () => {
+ if (stripeInstance.value) {
+ stripeLoaded.value = true
+
+ return
+ }
+
+ loadStripe(STRIPE_PUBLIC_KEY)
+ .then((stripe) => {
+ console.log('stripe loaded', stripe)
+ stripeInstance.value = stripe // Save the stripe instance
+ stripeLoaded.value = true
+ })
+ .catch((error) => {
+ console.error('stripe load error', error)
+ })
+ })
+
+ return { stripeLoaded, stripeInstance }
+}
diff --git a/src/config/stripe.ts b/src/config/stripe.ts
new file mode 100644
index 000000000..c51380908
--- /dev/null
+++ b/src/config/stripe.ts
@@ -0,0 +1,2 @@
+export const STRIPE_PUBLIC_KEY =
+ 'pk_test_51QMQglG0RyyEe7XHRvDnZREz1UjN8w3IM0X8klwml2yDf2JcCpHM7WzqFNK1UpXJRTo0jr7uUNDM5CD2syxGLojV00wrOHlZb9'
diff --git a/src/pages/billing/BillingPage.vue b/src/pages/billing/BillingPage.vue
index 42493ae96..ac5df669f 100644
--- a/src/pages/billing/BillingPage.vue
+++ b/src/pages/billing/BillingPage.vue
@@ -18,7 +18,7 @@
import MembeshipTier from './MembeshipTier.vue'
import PaymentInfo from './PaymentInfo.vue'
import { usePaymentCardsStore } from '../../stores/payment-cards'
-import Invoices from './Invoices.vue'
+import Invoices from './InvoicesStripe.vue'
const cardStore = usePaymentCardsStore()
cardStore.load()
diff --git a/src/pages/billing/InvoicesStripe.vue b/src/pages/billing/InvoicesStripe.vue
new file mode 100644
index 000000000..2369b2c93
--- /dev/null
+++ b/src/pages/billing/InvoicesStripe.vue
@@ -0,0 +1,64 @@
+
+
+
+ Invoices
+
+
+
+ {{ formatDate(item.created) }}
+
+
+ {{ formatCurrency(item.amount_due, item.currency) }}
+
+
+ Download
+
+
+
+
+
+
+
+ Load more
+ Loading...
+
+
+
+
+
diff --git a/src/pages/payments/PaymentsPage.vue b/src/pages/payments/PaymentsPage.vue
index 000978b29..da1e4b028 100644
--- a/src/pages/payments/PaymentsPage.vue
+++ b/src/pages/payments/PaymentsPage.vue
@@ -27,6 +27,6 @@
diff --git a/src/pages/payments/types.ts b/src/pages/payments/types.ts
index 5b037ad05..7c57dcf6f 100644
--- a/src/pages/payments/types.ts
+++ b/src/pages/payments/types.ts
@@ -1,3 +1,5 @@
+import type { Stripe } from 'stripe'
+
export enum PaymentSystemType {
Visa = 'visa',
MasterCard = 'mastercard',
@@ -5,13 +7,13 @@ export enum PaymentSystemType {
export const paymentSystemTypeOptions = Object.values(PaymentSystemType)
-export interface PaymentCard {
- id: string
- name: string
- isPrimary: boolean // show Primary badge
- paymentSystem: PaymentSystemType // Enum or union type for various payment systems
- cardNumberMasked: string // ****1679
- expirationDate: string // 09/24
+export type PaymentCard = Stripe.PaymentMethod.Card & {
+ id: string // Unique payment method ID from Stripe
+ isPrimary?: boolean // Custom property for UI logic (optional)
+ created: number // Unix timestamp
+ billingDetails: {
+ name: string
+ }
}
export interface BillingAddress {
diff --git a/src/pages/payments/widgets/my-cards/PaymentStripeCardAdd.vue b/src/pages/payments/widgets/my-cards/PaymentStripeCardAdd.vue
new file mode 100644
index 000000000..df27a63ba
--- /dev/null
+++ b/src/pages/payments/widgets/my-cards/PaymentStripeCardAdd.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+ Save Card
+
+
+
+
+
+
+
diff --git a/src/pages/payments/widgets/my-cards/PaymentStripeCardCreateModal.vue b/src/pages/payments/widgets/my-cards/PaymentStripeCardCreateModal.vue
new file mode 100644
index 000000000..879961070
--- /dev/null
+++ b/src/pages/payments/widgets/my-cards/PaymentStripeCardCreateModal.vue
@@ -0,0 +1,38 @@
+
+
+ Add payment card
+
+
+
+ Please use
+ Stripe test cards
+ for testing purposes.
+
+
+
+
+
+
+
diff --git a/src/pages/payments/widgets/my-cards/PaymentStripeCardList.vue b/src/pages/payments/widgets/my-cards/PaymentStripeCardList.vue
new file mode 100644
index 000000000..e02110b0e
--- /dev/null
+++ b/src/pages/payments/widgets/my-cards/PaymentStripeCardList.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
Important note
+
+ Please carefully read Product Terms before adding your new payment card
+
+
+
Add card
+
+
+
+
+
+
diff --git a/src/pages/pricing-plans/PricingPlans.vue b/src/pages/pricing-plans/PricingPlans.vue
index 4b1ed205e..557c8e628 100644
--- a/src/pages/pricing-plans/PricingPlans.vue
+++ b/src/pages/pricing-plans/PricingPlans.vue
@@ -23,7 +23,7 @@
:class="{
'md:!py-10 !bg-backgroundCardSecondary': plan.model === 'Advanced',
'!bg-backgroundCardPrimary': plan.model !== 'Advanced',
- 'ring-2 ring-primary ring-offset-2': plan.model === selectedPlan,
+ 'ring-2 ring-primary ring-offset-2': plan.model === selectedPlan.model,
}"
class="flex w-[326px] md:w-[349px] h-fit p-6 rounded-[13px]"
>
@@ -59,9 +59,9 @@
Select
@@ -69,34 +69,53 @@
+
diff --git a/src/services/stripe.ts b/src/services/stripe.ts
new file mode 100644
index 000000000..cb6084a13
--- /dev/null
+++ b/src/services/stripe.ts
@@ -0,0 +1,117 @@
+// Fetch payment cards from Stripe
+import type { Stripe } from 'stripe'
+
+const SERVER_BASE = ' http://localhost:3000'
+
+export const stripeFetchCards = async (customerId = 'cus_test_hardcoded_id') => {
+ try {
+ const response = await fetch(`${SERVER_BASE}/api/payment-method?customerId=${customerId}`)
+ if (!response.ok) {
+ throw new Error(`Error fetching payment methods: ${response.statusText}`)
+ }
+ const data = (await response.json()).data as Stripe.PaymentMethod[]
+ return data
+ } catch (error) {
+ console.error('stripeFetchCards error:', error)
+ throw error // Re-throw the error so it can be handled by the caller
+ }
+}
+
+export const stripeAddCard = async (paymentMethodId: string) => {
+ try {
+ const response = await fetch(`${SERVER_BASE}/api/payment-method`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ // customerId: 'cus_test_hardcoded_id',
+ paymentMethodId,
+ }),
+ })
+
+ if (!response.ok) {
+ throw new Error(`Error adding payment method: ${response.statusText}`)
+ }
+
+ const data = (await response.json()).data as Stripe.PaymentMethod
+
+ return data
+ } catch (error) {
+ console.error('stripeAddCard error:', error)
+ throw error // Re-throw the error so it can be handled by the caller
+ }
+}
+
+export const stripeRemoveCard = async (paymentMethodId: string) => {
+ try {
+ const response = await fetch(`${SERVER_BASE}/api/payment-method/${paymentMethodId}`, {
+ method: 'DELETE',
+ })
+
+ if (!response.ok) {
+ throw new Error(`Error removing payment method: ${response.statusText}`)
+ }
+ } catch (error) {
+ console.error('stripeRemoveCard error:', error)
+ throw error // Re-throw the error so it can be handled by the caller
+ }
+}
+
+export const stripeCreatePaymentIntent = async (payload: any) => {
+ try {
+ const response = await fetch(`${SERVER_BASE}/api/payment-intent`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ })
+
+ if (!response.ok) {
+ throw new Error(`Error creating payment intent: ${response.statusText}`)
+ }
+
+ return await response.json()
+ } catch (error) {
+ console.error('stripeRemoveCard error:', error)
+ throw error // Re-throw the error so it can be handled by the caller
+ }
+}
+
+export const stripeCreatePayment = async (payload: any) => {
+ try {
+ const response = await fetch(`${SERVER_BASE}/api/payment`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ })
+
+ if (!response.ok) {
+ throw new Error(`Error creating payment intent: ${response.statusText}`)
+ }
+
+ return await response.json()
+ } catch (error) {
+ console.error('stripeRemoveCard error:', error)
+ throw error // Re-throw the error so it can be handled by the caller
+ }
+}
+
+export const stripeFetchInvoices = async (startingAfter: string | null = null) => {
+ try {
+ // Construct the URL with query params
+ const url = new URL(`${SERVER_BASE}/api/invoices`)
+ if (startingAfter) {
+ url.searchParams.append('starting_after', startingAfter)
+ }
+
+ const response = await fetch(url.toString())
+ if (!response.ok) {
+ throw new Error(`Error fetching invoices: ${response.statusText}`)
+ }
+
+ const data = await response.json()
+
+ return data
+ } catch (error) {
+ console.error('stripeFetchInvoices error:', error)
+ throw error // Re-throw the error so it can be handled by the caller
+ }
+}
diff --git a/src/stores/invoices.ts b/src/stores/invoices.ts
new file mode 100644
index 000000000..2a6a2811d
--- /dev/null
+++ b/src/stores/invoices.ts
@@ -0,0 +1,69 @@
+import { defineStore } from 'pinia'
+import { stripeFetchInvoices } from '../services/stripe'
+
+export interface Invoice {
+ id: string
+ amount_due: number
+ currency: string
+ status: string
+ created: number
+}
+
+export const useInvoicesStore = defineStore({
+ id: 'invoices',
+ state: () => ({
+ invoices: [] as Invoice[],
+ loading: false,
+ hasMore: true,
+ lastInvoiceId: null as string | null, // The ID of the last invoice for pagination
+ }),
+ actions: {
+ async load() {
+ await this.loadInvoices(null)
+ },
+
+ async loadMore() {
+ if (!this.hasMore) return
+
+ await this.loadInvoices(this.lastInvoiceId)
+ },
+
+ async loadInvoices(startingAfter: string | null) {
+ // Avoid duplicate requests or unnecessary requests
+ if (this.loading) return
+
+ this.loading = true
+
+ try {
+ const response = await stripeFetchInvoices(startingAfter)
+ const { invoices, has_more } = response.data
+
+ const newInvoices = invoices.map((invoice: Invoice) => ({
+ id: invoice.id,
+ amount_due: invoice.amount_due,
+ currency: invoice.currency,
+ status: invoice.status,
+ created: invoice.created,
+ })) as Invoice[]
+ // Update invoices (only append if not the initial load)
+
+ this.invoices = [].concat(this.invoices, newInvoices)
+
+ this.hasMore = has_more
+ this.lastInvoiceId = invoices.length > 0 ? invoices[invoices.length - 1].id : this.lastInvoiceId
+ } catch (error) {
+ console.error('Error loading invoices:', error)
+ } finally {
+ this.loading = false
+ }
+ },
+
+ // Clear all invoices and reset state
+ clear() {
+ this.invoices = []
+ this.loading = false
+ this.hasMore = true
+ this.lastInvoiceId = null
+ },
+ },
+})
diff --git a/src/stores/stripe-cards.ts b/src/stores/stripe-cards.ts
new file mode 100644
index 000000000..b7ae5f4c2
--- /dev/null
+++ b/src/stores/stripe-cards.ts
@@ -0,0 +1,68 @@
+import { defineStore } from 'pinia'
+import { stripeAddCard, stripeFetchCards, stripeRemoveCard, stripeCreatePayment } from '../services/stripe'
+
+import type { PaymentCard } from '../pages/payments/types'
+
+export const useStripePaymentCardsStore = defineStore({
+ id: 'stripeCards',
+ state: () => ({
+ paymentCards: [] as PaymentCard[],
+ loading: false,
+ }),
+ actions: {
+ async load() {
+ this.loading = true
+ try {
+ const paymentMethods = await stripeFetchCards()
+ this.paymentCards = paymentMethods.map((paymentMethod) => ({
+ id: paymentMethod.id,
+ created: paymentMethod.created,
+ ...paymentMethod.card,
+ billingDetails: paymentMethod.billing_details,
+ })) as PaymentCard[]
+ } catch (error) {
+ console.error('Error loading payment cards:', error)
+ } finally {
+ this.loading = false
+ }
+ },
+ async create(paymentMethodId: string) {
+ try {
+ const paymentMethod = await stripeAddCard(paymentMethodId)
+ const card = {
+ id: paymentMethod.id,
+ created: paymentMethod.created,
+ ...paymentMethod.card,
+ billingDetails: paymentMethod.billing_details,
+ } as PaymentCard
+
+ this.paymentCards.push(card)
+ } catch (error) {
+ console.error('Error creating payment card:', error)
+ throw error
+ }
+ },
+ async remove(paymentMethodId: string) {
+ try {
+ await stripeRemoveCard(paymentMethodId)
+ this.paymentCards = this.paymentCards.filter((card) => card.id !== paymentMethodId)
+ } catch (error) {
+ console.error('Error removing payment card:', error)
+ }
+ },
+ async processPayment(paymentMethodId: string, amount: number) {
+ try {
+ const response = await stripeCreatePayment({
+ amount, // Replace with actual amount
+ currency: 'usd',
+ paymentMethod: paymentMethodId,
+ confirm: true,
+ })
+ return response
+ } catch (error) {
+ console.error('Error processing payment:', error)
+ throw error
+ }
+ },
+ },
+})
diff --git a/yarn.lock b/yarn.lock
index 9047bbeba..55dbc8f71 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2746,6 +2746,11 @@
type-fest "~2.19"
vue-component-type-helpers latest
+"@stripe/stripe-js@^4.10.0":
+ version "4.10.0"
+ resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-4.10.0.tgz#5c785f9a5a500113d69d98c16061e0addd1c0305"
+ integrity sha512-KrMOL+sH69htCIXCaZ4JluJ35bchuCCznyPyrbN8JXSGQfwBI1SuIEMZNwvy8L8ykj29t6sa5BAAiL7fNoLZ8A==
+
"@testing-library/dom@^9.0.0":
version "9.3.3"
resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz"
@@ -2980,6 +2985,13 @@
dependencies:
undici-types "~5.26.4"
+"@types/node@>=8.1.0":
+ version "22.9.0"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.0.tgz#b7f16e5c3384788542c72dc3d561a7ceae2c0365"
+ integrity sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==
+ dependencies:
+ undici-types "~6.19.8"
+
"@types/node@^18.0.0":
version "18.18.9"
resolved "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz"
@@ -3048,6 +3060,13 @@
"@types/mime" "*"
"@types/node" "*"
+"@types/stripe@^8.0.417":
+ version "8.0.417"
+ resolved "https://registry.yarnpkg.com/@types/stripe/-/stripe-8.0.417.tgz#b651677a9fc33be8ce8fd5bceadd7ca077214244"
+ integrity sha512-PTuqskh9YKNENnOHGVJBm4sM0zE8B1jZw1JIskuGAPkMB+OH236QeN8scclhYGPA4nG6zTtPXgwpXdp+HPDTVw==
+ dependencies:
+ stripe "*"
+
"@types/topojson-client@^3.1.4":
version "3.1.4"
resolved "https://registry.yarnpkg.com/@types/topojson-client/-/topojson-client-3.1.4.tgz#81b83f9ecd6542dc5c3df21967f992e3fe192c33"
@@ -7465,7 +7484,7 @@ puppeteer-core@^2.1.1:
rimraf "^2.6.1"
ws "^6.1.0"
-qs@6.13.0:
+qs@6.13.0, qs@^6.11.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
@@ -8243,6 +8262,14 @@ strip-json-comments@~2.0.1:
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz"
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
+stripe@*, stripe@^17.3.1:
+ version "17.3.1"
+ resolved "https://registry.yarnpkg.com/stripe/-/stripe-17.3.1.tgz#901e91e85f6bdbbe10094debab13ef2a9d4c0fc0"
+ integrity sha512-E9/u+GFBPkYnTmfFCoKX3+gP4R3SkZoGunHe4cw9J+sqkj5uxpLFf1LscuI9BuEyIQ0PFAgPTHavgQwRtOvnag==
+ dependencies:
+ "@types/node" ">=8.1.0"
+ qs "^6.11.0"
+
sucrase@^3.32.0:
version "3.34.0"
resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz"
@@ -8583,6 +8610,11 @@ undici-types@~5.26.4:
resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
+undici-types@~6.19.8:
+ version "6.19.8"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
+ integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
+
unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz"
@@ -8831,6 +8863,11 @@ vue-inbrowser-compiler-independent-utils@^4.69.0:
dependencies:
"@vue/devtools-api" "^6.5.0"
+vue-stripe-js@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/vue-stripe-js/-/vue-stripe-js-1.0.3.tgz#35c7f4f6e4e89caea7dcf48fe05526021bb57436"
+ integrity sha512-vp9+EJYY0NPJsnyQJ+FsGJ85m/EaFQM0J0m6+y+KjEPOq1OoPp5jkA57UlfHD6fKJ28STeafNnQGoCMdijF5yg==
+
vue-tsc@^2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.1.6.tgz#d93fdc617da6546674301a746fd7089ea6d4543d"