Skip to content

Commit c8fccfb

Browse files
chore: wip
1 parent 165c0dc commit c8fccfb

File tree

8 files changed

+266
-2
lines changed

8 files changed

+266
-2
lines changed

app/Actions/Payment/CreatePaymentIntentAction.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@ export default new Action({
99
async handle(request: RequestInstance) {
1010
const amount = Number(request.get('amount'))
1111

12+
// TODO: Implement a fetch customer by find by email
13+
// const customer = await stripe.customer.create({
14+
// email: 'gtorregosa@gmail.com',
15+
// name: 'John Doe',
16+
// });
17+
1218
const paymentIntent = await stripe.paymentIntent.create({
19+
customer: 'cus_R5DJaEyyeKKlAN',
1320
amount,
1421
currency: 'usd',
22+
description: 'Subscription to Stacks Pro',
1523
payment_method_types: ['card'],
1624
})
1725

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Action } from '@stacksjs/actions'
2+
import { stripe } from '@stacksjs/payments'
3+
4+
export default new Action({
5+
name: 'CreateSubscriptionAction',
6+
description: 'Create Subscription for stripe',
7+
method: 'POST',
8+
async handle() {
9+
const subscription = await stripe.subscription.create({
10+
customer: 'cus_R5DJaEyyeKKlAN',
11+
items: [
12+
{
13+
price: 'price_1QCjMXBv6MhUdo23Pvb5dwUd',
14+
},
15+
],
16+
payment_behavior: 'default_incomplete',
17+
expand: ['latest_invoice.payment_intent'],
18+
})
19+
20+
// Step 3: Get the PaymentIntent for the first invoice from the subscription
21+
const latestInvoice = subscription.latest_invoice
22+
const paymentIntent = typeof latestInvoice === 'object' ? latestInvoice?.payment_intent : undefined
23+
24+
// Step 4: Pass the client_secret to the front end for Stripe Elements
25+
return paymentIntent
26+
},
27+
})

bun.lockb

440 Bytes
Binary file not shown.

routes/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ route.get('/install', 'Actions/InstallAction')
2525
route.post('/ai/ask', 'Actions/AI/AskAction')
2626
route.post('/ai/summary', 'Actions/AI/SummaryAction')
2727

28-
route.post('/create-payment-intent', 'Actions/Payment/CreatePaymentIntentAction')
28+
route.post('/stripe/create-payment-intent', 'Actions/Payment/CreatePaymentIntentAction')
29+
route.post('/stripe/create-subscription', 'Actions/Payment/CreateSubscriptionAction')
2930

3031
// route.group('/some-path', async () => {...})
3132
// route.action('/example') // equivalent to `route.get('/example', 'ExampleAction')`

storage/framework/core/components/stripe/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ declare module 'vue' {
1313
RouterView: typeof import('vue-router')['RouterView']
1414
Starport: typeof import('vue-starport')['Starport']
1515
StarportCarrier: typeof import('vue-starport')['StarportCarrier']
16+
Subscription: typeof import('./src/components/Subscription.vue')['default']
1617
}
1718
}

storage/framework/core/components/stripe/src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ const products = ref([{
1212

1313
<template>
1414
<div>
15-
<Checkout mode="one-time" :products="products" redirect-url="https://google.com" />
15+
<Checkout mode="subscription" :products="products" redirect-url="https://google.com" />
1616
</div>
1717
</template>
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
<script setup lang="ts">
2+
import { onMounted, ref } from 'vue'
3+
4+
import { loadStripe, type StripeElements, type Stripe } from "@stripe/stripe-js";
5+
6+
7+
interface Products {
8+
name: string
9+
price: number,
10+
images: string
11+
}
12+
13+
interface Props {
14+
products: Products[]
15+
redirectUrl: string,
16+
applePay?: boolean
17+
googlePay?: boolean
18+
}
19+
20+
const clientSecret = ref('')
21+
const props = defineProps<Props>();
22+
23+
let elements: StripeElements | null = null;
24+
let stripe: Stripe | null = null
25+
const publicKey = ''
26+
27+
const products = props.products
28+
29+
onMounted(async () => {
30+
stripe = await loadStripe(publicKey);
31+
32+
await createPaymentIntent();
33+
await loadElements(); // Load both address and payment elements
34+
});
35+
36+
async function createPaymentIntent() {
37+
const url = 'http://localhost:3008/stripe/create-subscription';
38+
39+
const body = { amount: 99900, quantity: 1 };
40+
41+
const response = await fetch(url, {
42+
method: 'POST',
43+
headers: {
44+
'Content-Type': 'application/json',
45+
'Accept': 'application/json',
46+
},
47+
body: JSON.stringify(body),
48+
});
49+
50+
const paymentIntent: any = await response.json();
51+
clientSecret.value = paymentIntent.client_secret;
52+
}
53+
54+
async function loadElements() {
55+
if (stripe) {
56+
const appearance = { /* appearance options */ };
57+
const options = { mode: 'billing' };
58+
59+
// Create the elements instance once
60+
elements = stripe.elements({ clientSecret: clientSecret.value, appearance });
61+
62+
// Create and mount the address element
63+
const addressElement = elements.create('address', options);
64+
addressElement.mount('#address-element')
65+
66+
// Create and mount the payment element
67+
const paymentElement = elements.create('payment')
68+
paymentElement.mount("#payment-element")
69+
}
70+
}
71+
72+
73+
async function handleSubmit() {
74+
if (stripe && elements) {
75+
const { error } = await stripe.confirmPayment({
76+
elements,
77+
confirmParams: {
78+
return_url: props.redirectUrl, // Redirect URL after payment
79+
},
80+
})
81+
}
82+
}
83+
</script>
84+
85+
<template>
86+
<div class="bg-gray-50">
87+
<div class="mx-auto max-w-2xl px-4 pb-24 pt-16 sm:px-6 lg:max-w-7xl lg:px-8">
88+
<h2 class="sr-only">Checkout</h2>
89+
90+
<form class="lg:grid lg:grid-cols-2 lg:gap-x-12 xl:gap-x-16">
91+
<div>
92+
<div>
93+
<h2 class="text-lg font-medium text-gray-900">Contact information</h2>
94+
95+
<div class="mt-4">
96+
<label for="email-address" class="block text-sm font-medium text-gray-700">Email address</label>
97+
<div class="mt-1">
98+
<input type="email" id="email-address" name="email-address" autocomplete="email" class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border">
99+
</div>
100+
</div>
101+
</div>
102+
103+
<div class="mt-10 border-t border-gray-200 pt-10">
104+
<h2 class="text-lg font-medium text-gray-900">Billing information</h2>
105+
106+
<div id="address-element" class="mt-12">
107+
108+
</div>
109+
</div>
110+
111+
<!-- Payment -->
112+
<div class="mt-10 border-t border-gray-200 pt-10">
113+
<h2 class="text-lg font-medium text-gray-900">Payment</h2>
114+
115+
<fieldset class="mt-4">
116+
<legend class="sr-only">Payment type</legend>
117+
<div class="space-y-4 sm:flex sm:items-center sm:space-x-10 sm:space-y-0">
118+
<div class="flex items-center">
119+
<input id="credit-card" name="payment-type" type="radio" checked class="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-500 form">
120+
<label for="credit-card" class="ml-3 block text-sm font-medium text-gray-700">Credit card</label>
121+
</div>
122+
123+
</div>
124+
</fieldset>
125+
126+
<div class="mt-6">
127+
<div id="payment-element" class="border rounded-md bg-gray-50 p-3" />
128+
</div>
129+
130+
</div>
131+
</div>
132+
133+
<!-- Order summary -->
134+
<div class="mt-10 lg:mt-0">
135+
<h2 class="text-lg font-medium text-gray-900">Order summary</h2>
136+
137+
<div class="mt-4 rounded-lg border border-gray-200 bg-white shadow-sm">
138+
<h3 class="sr-only">Items in your cart</h3>
139+
<ul role="list" class="divide-y divide-gray-200">
140+
<li class="flex px-4 py-6 sm:px-6" v-for="product in products">
141+
<div class="flex-shrink-0">
142+
<img :src="product?.images" alt="Front of men&#039;s Basic Tee in black." class="w-50 rounded-md">
143+
</div>
144+
145+
<div class="ml-6 flex flex-1 flex-col">
146+
<div class="flex">
147+
<div class="min-w-0 flex-1">
148+
<h4 class="text-sm">
149+
<a href="#" class="font-medium text-gray-700 hover:text-gray-800">{{ product?.name }}</a>
150+
</h4>
151+
<p class="mt-1 text-sm text-gray-500">..</p>
152+
<p class="mt-1 text-sm text-gray-500">..</p>
153+
</div>
154+
155+
<div class="ml-4 flow-root flex-shrink-0">
156+
<button type="button" class="-m-2.5 flex items-center justify-center bg-white p-2.5 text-gray-400 hover:text-gray-500">
157+
<span class="sr-only">Remove</span>
158+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" data-slot="icon">
159+
<path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 0 0 6 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 1 0 .23 1.482l.149-.022.841 10.518A2.75 2.75 0 0 0 7.596 19h4.807a2.75 2.75 0 0 0 2.742-2.53l.841-10.52.149.023a.75.75 0 0 0 .23-1.482A41.03 41.03 0 0 0 14 4.193V3.75A2.75 2.75 0 0 0 11.25 1h-2.5ZM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4ZM8.58 7.72a.75.75 0 0 0-1.5.06l.3 7.5a.75.75 0 1 0 1.5-.06l-.3-7.5Zm4.34.06a.75.75 0 1 0-1.5-.06l-.3 7.5a.75.75 0 1 0 1.5.06l.3-7.5Z" clip-rule="evenodd" />
160+
</svg>
161+
</button>
162+
</div>
163+
</div>
164+
165+
<div class="flex flex-1 items-end justify-between pt-2">
166+
<p class="mt-1 text-sm font-medium text-gray-900">${{ (product?.price / 100).toFixed(2) }}</p>
167+
168+
<div class="ml-4">
169+
<label for="quantity" class="sr-only">Quantity</label>
170+
<select id="quantity" name="quantity" class="rounded-md border border-gray-300 text-left text-base font-medium text-gray-700 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm">
171+
<option value="1">1</option>
172+
<option value="2">2</option>
173+
<option value="3">3</option>
174+
<option value="4">4</option>
175+
<option value="5">5</option>
176+
<option value="6">6</option>
177+
<option value="7">7</option>
178+
<option value="8">8</option>
179+
</select>
180+
</div>
181+
</div>
182+
</div>
183+
</li>
184+
185+
</ul>
186+
<dl class="space-y-6 border-t border-gray-200 px-4 py-6 sm:px-6">
187+
<div class="flex items-center justify-between">
188+
<dt class="text-sm">Subtotal</dt>
189+
<dd class="text-sm font-medium text-gray-900">$950.00</dd>
190+
</div>
191+
<div class="flex items-center justify-between">
192+
<dt class="text-sm">Shipping</dt>
193+
<dd class="text-sm font-medium text-gray-900">$45.00</dd>
194+
</div>
195+
<div class="flex items-center justify-between">
196+
<dt class="text-sm">Taxes</dt>
197+
<dd class="text-sm font-medium text-gray-900">$5.52</dd>
198+
</div>
199+
<div class="flex items-center justify-between border-t border-gray-200 pt-6">
200+
<dt class="text-base font-medium">Total</dt>
201+
<dd class="text-base font-medium text-gray-900">$999.00</dd>
202+
</div>
203+
</dl>
204+
205+
<div class="border-t border-gray-200 px-4 py-6 sm:px-6">
206+
<button @click="handleSubmit" type="button" class="w-full rounded-md border border-transparent bg-indigo-600 px-4 py-3 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-50">Confirm order</button>
207+
</div>
208+
</div>
209+
</div>
210+
</form>
211+
</div>
212+
</div>
213+
</template>

storage/framework/core/payments/src/drivers/stripe.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,20 @@ export const charge: Charge = (() => {
9797
return { create, update, retrieve, capture }
9898
})()
9999

100+
101+
export interface Subscription {
102+
create: (params: Stripe.SubscriptionCreateParams) => Promise<Stripe.Response<Stripe.Subscription>>
103+
}
104+
105+
106+
export const subscription: Subscription = (() => {
107+
async function create(params: Stripe.SubscriptionCreateParams) {
108+
return await client.subscriptions.create(params)
109+
}
110+
111+
return { create }
112+
})()
113+
100114
export interface BalanceTransactions {
101115
retrieve: (stripeId: string) => Promise<Stripe.Response<Stripe.BalanceTransaction>>
102116
list: (limit: number) => Promise<Stripe.Response<Stripe.ApiList<Stripe.BalanceTransaction>>>

0 commit comments

Comments
 (0)