@virgodev/iap 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/StripeCheckout.vue +91 -54
- package/components/StripePopup.vue +19 -40
- package/index.ts +12 -0
- package/package.json +1 -1
- package/stores/iap.ts +32 -9
- package/utils.ts +35 -0
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<Message
|
|
3
|
+
v-if="!sessionResponse && messageText"
|
|
4
|
+
id="payment-message"
|
|
5
|
+
:class="{ hidden: !messageText }"
|
|
6
|
+
class="p-1 message"
|
|
7
|
+
severity="error"
|
|
8
|
+
>
|
|
9
|
+
<b>Failed to create payment session. Please try again later.</b>
|
|
10
|
+
<div>{{ messageText }}</div>
|
|
11
|
+
</Message>
|
|
12
|
+
<div v-else-if="!sessionResponse" id="payment-form p-4">Loading...</div>
|
|
3
13
|
<form
|
|
4
14
|
v-else
|
|
5
15
|
id="payment-form"
|
|
6
|
-
class="p-3-t flex-
|
|
16
|
+
class="p-3-t flex-wrap gap-2"
|
|
17
|
+
:class="{
|
|
18
|
+
'flex-row': !props.vertical,
|
|
19
|
+
'flex-column': props.vertical,
|
|
20
|
+
}"
|
|
7
21
|
@submit.prevent="handleSubmit"
|
|
8
22
|
>
|
|
9
|
-
<
|
|
10
|
-
v-if="
|
|
23
|
+
<Message
|
|
24
|
+
v-if="messageText"
|
|
11
25
|
id="payment-message"
|
|
12
|
-
:class="{ hidden: !showMessage }"
|
|
13
26
|
class="message"
|
|
27
|
+
severity="error"
|
|
14
28
|
>
|
|
15
29
|
{{ messageText }}
|
|
16
|
-
</
|
|
30
|
+
</Message>
|
|
17
31
|
|
|
18
32
|
<div class="cart flex-column flex-stretch p-1">
|
|
19
33
|
<div v-for="item in cart.items" :key="item.id" class="flex-column p-1">
|
|
@@ -48,11 +62,11 @@
|
|
|
48
62
|
<!-- Stripe.js injects the Payment Element -->
|
|
49
63
|
</div>
|
|
50
64
|
|
|
51
|
-
<slot name="
|
|
52
|
-
<div class="flex-row justify-
|
|
53
|
-
<
|
|
65
|
+
<slot name="default">
|
|
66
|
+
<div class="flex-row justify-right p-2">
|
|
67
|
+
<Button id="submit" :disabled="isLoading" type="submit">
|
|
54
68
|
Pay Now
|
|
55
|
-
</
|
|
69
|
+
</Button>
|
|
56
70
|
</div>
|
|
57
71
|
</slot>
|
|
58
72
|
</div>
|
|
@@ -65,62 +79,43 @@ import type {
|
|
|
65
79
|
StripeCheckoutLoadActionsResult,
|
|
66
80
|
} from "@stripe/stripe-js";
|
|
67
81
|
import api from "@virgodev/bazaar/functions/api";
|
|
68
|
-
import { onMounted, ref } from "vue";
|
|
82
|
+
import { onMounted, ref, watch, nextTick } from "vue";
|
|
69
83
|
import { ProductType, useIapStore } from "../index";
|
|
70
84
|
import type { StripeCart } from "../stores/iap";
|
|
85
|
+
import Message from "primevue/message";
|
|
86
|
+
import Button from "primevue/button";
|
|
71
87
|
|
|
72
88
|
const props = defineProps<{
|
|
73
89
|
cart: StripeCart;
|
|
74
90
|
returnUrl: string;
|
|
91
|
+
email: string;
|
|
92
|
+
vertical: false;
|
|
75
93
|
}>();
|
|
76
94
|
|
|
77
95
|
const iap = useIapStore();
|
|
78
96
|
|
|
79
97
|
const isLoading = ref<boolean>(false);
|
|
80
|
-
const showMessage = ref(false);
|
|
81
98
|
const messageText = ref("");
|
|
82
99
|
const sessionResponse = ref<any>();
|
|
83
100
|
|
|
84
101
|
let checkout: StripeCheckout | null = null;
|
|
85
102
|
let loadActionsResult: StripeCheckoutLoadActionsResult | null = null;
|
|
103
|
+
let paymentElement = undefined;
|
|
86
104
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
setTimeout(() => {
|
|
92
|
-
showMessage.value = false;
|
|
93
|
-
messageText.value = "";
|
|
94
|
-
}, 4000);
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const setLoading = (loading: boolean) => {
|
|
98
|
-
isLoading.value = loading;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const handleSubmit = async () => {
|
|
102
|
-
setLoading(true);
|
|
103
|
-
|
|
104
|
-
if (loadActionsResult?.type === "success") {
|
|
105
|
-
const result = await loadActionsResult.actions.confirm({
|
|
106
|
-
returnUrl:
|
|
107
|
-
props.returnUrl + "?stripe_session_id=" + sessionResponse.value.id,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
if (result.type === "error") {
|
|
111
|
-
showPaymentMessage(`${result.error.code}: ${result.error.message}`);
|
|
112
|
-
} else {
|
|
113
|
-
// TODO: verify with server
|
|
114
|
-
// TODO: connect purchaseItem with a promise resolve function
|
|
115
|
-
}
|
|
105
|
+
watch(
|
|
106
|
+
() => props.cart,
|
|
107
|
+
() => {
|
|
108
|
+
getSession();
|
|
116
109
|
}
|
|
117
|
-
|
|
118
|
-
setLoading(false);
|
|
119
|
-
};
|
|
110
|
+
);
|
|
120
111
|
|
|
121
112
|
onMounted(async () => {
|
|
122
|
-
|
|
123
|
-
|
|
113
|
+
getSession();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
async function getSession() {
|
|
117
|
+
if (iap.stripe.client) {
|
|
118
|
+
try {
|
|
124
119
|
// Fetch client secret from backend
|
|
125
120
|
const clientSecretResponse = await api({
|
|
126
121
|
url: "purchases/session/",
|
|
@@ -135,7 +130,7 @@ onMounted(async () => {
|
|
|
135
130
|
if (clientSecretResponse.ok) {
|
|
136
131
|
sessionResponse.value = clientSecretResponse.body;
|
|
137
132
|
|
|
138
|
-
checkout = await iap.stripe.initCheckout({
|
|
133
|
+
checkout = await iap.stripe.client.initCheckout({
|
|
139
134
|
clientSecret: sessionResponse.value.client_secret,
|
|
140
135
|
elementsOptions: { appearance: { theme: "stripe" } },
|
|
141
136
|
});
|
|
@@ -151,21 +146,63 @@ onMounted(async () => {
|
|
|
151
146
|
showPaymentMessage("Failed to load payment actions");
|
|
152
147
|
}
|
|
153
148
|
|
|
154
|
-
|
|
149
|
+
if (paymentElement) {
|
|
150
|
+
await paymentElement.destroy();
|
|
151
|
+
await nextTick();
|
|
152
|
+
}
|
|
153
|
+
paymentElement = checkout.createPaymentElement();
|
|
155
154
|
paymentElement.mount("#payment-element");
|
|
155
|
+
} else {
|
|
156
|
+
const errorContent = clientSecretResponse.body.error;
|
|
157
|
+
iap.stripe.error = new Error(errorContent);
|
|
158
|
+
messageText.value = errorContent;
|
|
156
159
|
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
iap.stripe.error = error;
|
|
162
|
+
console.error("Error initializing checkout:", error);
|
|
163
|
+
showPaymentMessage("Failed to initialize payment form");
|
|
157
164
|
}
|
|
158
|
-
} catch (error) {
|
|
159
|
-
console.error("Error initializing checkout:", error);
|
|
160
|
-
showPaymentMessage("Failed to initialize payment form");
|
|
161
165
|
}
|
|
162
|
-
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function showPaymentMessage(messageContent: string) {
|
|
169
|
+
messageText.value = messageContent;
|
|
170
|
+
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
messageText.value = "";
|
|
173
|
+
}, 8000);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function setLoading(loading: boolean) {
|
|
177
|
+
isLoading.value = loading;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function handleSubmit() {
|
|
181
|
+
setLoading(true);
|
|
182
|
+
|
|
183
|
+
if (loadActionsResult?.type === "success") {
|
|
184
|
+
const result = await loadActionsResult.actions.confirm({
|
|
185
|
+
email: props.email,
|
|
186
|
+
returnUrl:
|
|
187
|
+
props.returnUrl + "?stripe_session_id=" + sessionResponse.value.id,
|
|
188
|
+
});
|
|
189
|
+
if (result.type === "error") {
|
|
190
|
+
showPaymentMessage(`${result.error.code}: ${result.error.message}`);
|
|
191
|
+
} else {
|
|
192
|
+
// TODO: verify with server
|
|
193
|
+
// TODO: connect purchaseItem with a promise resolve function
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
setLoading(false);
|
|
198
|
+
}
|
|
163
199
|
</script>
|
|
164
200
|
|
|
165
201
|
<style scoped>
|
|
166
202
|
#payment-form {
|
|
167
|
-
min-width:
|
|
168
|
-
|
|
203
|
+
min-width: 300px;
|
|
204
|
+
width: 600px;
|
|
205
|
+
flex: 1 1 600px;
|
|
169
206
|
}
|
|
170
207
|
.cart {
|
|
171
208
|
min-width: 250px;
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<Dialog
|
|
3
|
-
v-if="iap.
|
|
4
|
-
:visible="!!iap.
|
|
3
|
+
v-if="iap.stripe.status && iap.stripe.cart"
|
|
4
|
+
:visible="!!iap.stripe.cart"
|
|
5
5
|
:dismissable-mask="true"
|
|
6
6
|
:modal="true"
|
|
7
7
|
:show-header="false"
|
|
8
8
|
@update:visible="closeDialog"
|
|
9
9
|
>
|
|
10
10
|
<div
|
|
11
|
-
v-if="iap.
|
|
11
|
+
v-if="iap.stripe.status === 'verified' && iap.stripe.cart"
|
|
12
12
|
class="complete flex-column justify-center align-items-center p-3"
|
|
13
13
|
>
|
|
14
14
|
<h2 class="text-center p-1">Payment Complete!</h2>
|
|
15
15
|
<i class="pi pi-check-circle text-success p-2 text-center" />
|
|
16
16
|
<div class="cart p-3-x p-1-y">
|
|
17
|
-
<div v-for="item of iap.
|
|
17
|
+
<div v-for="item of iap.stripe.cart.items" class="flex-row">
|
|
18
18
|
<div class="flex-stretch">
|
|
19
19
|
<b>{{ item.product?.name }}</b>
|
|
20
20
|
</div>
|
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
</div>
|
|
23
23
|
</div>
|
|
24
24
|
</div>
|
|
25
|
-
<StripeCheckout
|
|
25
|
+
<StripeCheckout
|
|
26
|
+
v-else
|
|
27
|
+
:cart="iap.stripe.cart"
|
|
28
|
+
:return-url="returnUrl"
|
|
29
|
+
:email="props.email"
|
|
30
|
+
>
|
|
26
31
|
<template #button>
|
|
27
32
|
<div class="flex-row justify-stretch p-1-y">
|
|
28
33
|
<Button type="submit" class="flex-stretch">Pay Now</Button>
|
|
@@ -43,50 +48,24 @@ const router = useRouter();
|
|
|
43
48
|
const iap = useIapStore();
|
|
44
49
|
const visible = ref(false);
|
|
45
50
|
|
|
51
|
+
const props = defineProps<{ email: string }>();
|
|
52
|
+
|
|
46
53
|
const returnUrl = computed(() => {
|
|
47
54
|
return `${location.protocol}//${location.host}${router.currentRoute.value.fullPath}`;
|
|
48
55
|
});
|
|
49
56
|
|
|
50
|
-
router.beforeResolve(async (to, from, next) => {
|
|
51
|
-
if (to.query.stripe_session_id) {
|
|
52
|
-
const cart = iap.stripeCart;
|
|
53
|
-
await nextTick();
|
|
54
|
-
if (cart && cart.items.length > 0) {
|
|
55
|
-
const item = cart.items[0];
|
|
56
|
-
const transaction = {
|
|
57
|
-
products: [{ id: item.product.id }],
|
|
58
|
-
platform: Platform.STRIPE,
|
|
59
|
-
transactionId: `${to.query.stripe_session_id}`,
|
|
60
|
-
purchaseId: item.product.id,
|
|
61
|
-
finish: function (): void {},
|
|
62
|
-
};
|
|
63
|
-
const result = await iap.verify(transaction);
|
|
64
|
-
if (result) {
|
|
65
|
-
iap.stripeResult = transaction;
|
|
66
|
-
iap.stripeStatus = "verified";
|
|
67
|
-
delete to.query.stripe_session_id;
|
|
68
|
-
}
|
|
69
|
-
next(to);
|
|
70
|
-
} else {
|
|
71
|
-
console.warn("Found stripe session, but cart is empty");
|
|
72
|
-
next();
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
next();
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
57
|
onMounted(() => {});
|
|
80
58
|
|
|
81
59
|
function closeDialog(v: boolean): void {
|
|
82
60
|
if (v === false) {
|
|
83
|
-
if (iap.
|
|
84
|
-
iap.
|
|
61
|
+
if (iap.stripe.callback) {
|
|
62
|
+
iap.stripe.callback(undefined);
|
|
85
63
|
}
|
|
86
|
-
iap.
|
|
87
|
-
iap.
|
|
88
|
-
iap.
|
|
89
|
-
iap.
|
|
64
|
+
iap.stripe.callback = undefined;
|
|
65
|
+
iap.stripe.result = undefined;
|
|
66
|
+
iap.stripe.cart = undefined;
|
|
67
|
+
iap.stripe.status = undefined;
|
|
68
|
+
iap.stripe.error = undefined;
|
|
90
69
|
visible.value = false;
|
|
91
70
|
}
|
|
92
71
|
}
|
package/index.ts
CHANGED
|
@@ -3,3 +3,15 @@ export { Platform, ProductType, useIapStore } from "./stores/iap";
|
|
|
3
3
|
export { default as StripeCheckout } from "./components/StripeCheckout.vue";
|
|
4
4
|
export { default as StripeComplete } from "./components/StripeComplete.vue";
|
|
5
5
|
export { default as StripePopup } from "./components/StripePopup.vue";
|
|
6
|
+
|
|
7
|
+
import { catchVerifiedPayments } from "./utils";
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
install(app, options) {
|
|
11
|
+
if (!options.router) {
|
|
12
|
+
console.error("Vue Router is required for the IAP plugin.");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
catchVerifiedPayments(options.router);
|
|
16
|
+
},
|
|
17
|
+
};
|
package/package.json
CHANGED
package/stores/iap.ts
CHANGED
|
@@ -128,6 +128,7 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
128
128
|
const stripeResult = ref<undefined | Transaction>();
|
|
129
129
|
const stripeCallback =
|
|
130
130
|
ref<(t: Transaction | undefined) => Transaction | undefined>();
|
|
131
|
+
const stripeError = ref<undefined | string>();
|
|
131
132
|
|
|
132
133
|
const storePlatform = computed(() => {
|
|
133
134
|
if (platform.value === "android") {
|
|
@@ -173,7 +174,8 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
173
174
|
|
|
174
175
|
store.initialize();
|
|
175
176
|
} else {
|
|
176
|
-
console.
|
|
177
|
+
console.debug("CdvPurchase not found, skipping ios/android payments");
|
|
178
|
+
isReady.value = true;
|
|
177
179
|
}
|
|
178
180
|
}
|
|
179
181
|
|
|
@@ -232,6 +234,8 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
232
234
|
}
|
|
233
235
|
|
|
234
236
|
async function verify(p: Transaction) {
|
|
237
|
+
await ready();
|
|
238
|
+
|
|
235
239
|
let productId = p.products[0].id;
|
|
236
240
|
const product = products.value.find((p) => p.id === productId);
|
|
237
241
|
if (product) {
|
|
@@ -268,7 +272,22 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
268
272
|
return store.products;
|
|
269
273
|
}
|
|
270
274
|
|
|
275
|
+
function ready(): boolean {
|
|
276
|
+
return new Promise((resolve) => {
|
|
277
|
+
watch(
|
|
278
|
+
isReady,
|
|
279
|
+
(ready) => {
|
|
280
|
+
if (isReady.value) {
|
|
281
|
+
resolve(ready);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
{ immediate: true }
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
271
289
|
return {
|
|
290
|
+
products,
|
|
272
291
|
platform,
|
|
273
292
|
storePlatform,
|
|
274
293
|
initialize,
|
|
@@ -277,14 +296,18 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
277
296
|
findPurchases,
|
|
278
297
|
verify,
|
|
279
298
|
purchases,
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
stripe
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
299
|
+
ready,
|
|
300
|
+
|
|
301
|
+
stripe: {
|
|
302
|
+
key: STRIP_KEY,
|
|
303
|
+
client: stripe,
|
|
304
|
+
loaded: stripeLoaded,
|
|
305
|
+
cart: stripeCart,
|
|
306
|
+
status: stripeStatus,
|
|
307
|
+
result: stripeResult,
|
|
308
|
+
callback: stripeCallback,
|
|
309
|
+
error: stripeError,
|
|
310
|
+
},
|
|
288
311
|
};
|
|
289
312
|
});
|
|
290
313
|
|
package/utils.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useRouter } from "vue-router";
|
|
2
|
+
import { useIapStore, Platform } from "./stores/iap";
|
|
3
|
+
import { nextTick } from "vue";
|
|
4
|
+
|
|
5
|
+
export function catchVerifiedPayments(router) {
|
|
6
|
+
router.beforeResolve(async (to, from, next) => {
|
|
7
|
+
if (to.query.stripe_session_id) {
|
|
8
|
+
const iap = useIapStore();
|
|
9
|
+
const cart = iap.stripe.cart;
|
|
10
|
+
await nextTick();
|
|
11
|
+
if (cart && cart.items.length > 0) {
|
|
12
|
+
const item = cart.items[0];
|
|
13
|
+
const transaction = {
|
|
14
|
+
products: [{ id: item.product.id }],
|
|
15
|
+
platform: Platform.STRIPE,
|
|
16
|
+
transactionId: `${to.query.stripe_session_id}`,
|
|
17
|
+
purchaseId: item.product.id,
|
|
18
|
+
finish: function (): void {},
|
|
19
|
+
};
|
|
20
|
+
const result = await iap.verify(transaction);
|
|
21
|
+
if (result) {
|
|
22
|
+
iap.stripe.result = transaction;
|
|
23
|
+
iap.stripe.status = "verified";
|
|
24
|
+
delete to.query.stripe_session_id;
|
|
25
|
+
}
|
|
26
|
+
next(to);
|
|
27
|
+
} else {
|
|
28
|
+
console.warn("Found stripe session, but cart is empty");
|
|
29
|
+
next();
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
next();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|