@virgodev/iap 1.0.2 → 1.0.4
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 +112 -59
- 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,51 @@ 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";
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
import Message from "primevue/message";
|
|
86
|
+
import Button from "primevue/button";
|
|
87
|
+
|
|
88
|
+
const props = withDefaults(
|
|
89
|
+
defineProps<{
|
|
90
|
+
cart: StripeCart;
|
|
91
|
+
returnUrl: string;
|
|
92
|
+
email: string;
|
|
93
|
+
vertical: boolean;
|
|
94
|
+
}>(),
|
|
95
|
+
{ vertical: false }
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const emits = defineEmits<{
|
|
99
|
+
start: [];
|
|
100
|
+
end: [string];
|
|
75
101
|
}>();
|
|
76
102
|
|
|
77
103
|
const iap = useIapStore();
|
|
78
104
|
|
|
79
105
|
const isLoading = ref<boolean>(false);
|
|
80
|
-
const showMessage = ref(false);
|
|
81
106
|
const messageText = ref("");
|
|
82
|
-
const sessionResponse = ref<any>();
|
|
107
|
+
const sessionResponse: any = ref<any>();
|
|
83
108
|
|
|
84
109
|
let checkout: StripeCheckout | null = null;
|
|
85
110
|
let loadActionsResult: StripeCheckoutLoadActionsResult | null = null;
|
|
111
|
+
let paymentElement = undefined;
|
|
86
112
|
|
|
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
|
-
}
|
|
113
|
+
watch(
|
|
114
|
+
() => props.cart,
|
|
115
|
+
() => {
|
|
116
|
+
getSession();
|
|
116
117
|
}
|
|
117
|
-
|
|
118
|
-
setLoading(false);
|
|
119
|
-
};
|
|
118
|
+
);
|
|
120
119
|
|
|
121
120
|
onMounted(async () => {
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
getSession();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
async function getSession() {
|
|
125
|
+
if (iap.stripe.client) {
|
|
126
|
+
try {
|
|
124
127
|
// Fetch client secret from backend
|
|
125
128
|
const clientSecretResponse = await api({
|
|
126
129
|
url: "purchases/session/",
|
|
@@ -135,7 +138,7 @@ onMounted(async () => {
|
|
|
135
138
|
if (clientSecretResponse.ok) {
|
|
136
139
|
sessionResponse.value = clientSecretResponse.body;
|
|
137
140
|
|
|
138
|
-
checkout = await iap.stripe.initCheckout({
|
|
141
|
+
checkout = await iap.stripe.client.initCheckout({
|
|
139
142
|
clientSecret: sessionResponse.value.client_secret,
|
|
140
143
|
elementsOptions: { appearance: { theme: "stripe" } },
|
|
141
144
|
});
|
|
@@ -151,21 +154,71 @@ onMounted(async () => {
|
|
|
151
154
|
showPaymentMessage("Failed to load payment actions");
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
|
|
157
|
+
if (paymentElement) {
|
|
158
|
+
await paymentElement.destroy();
|
|
159
|
+
await nextTick();
|
|
160
|
+
}
|
|
161
|
+
paymentElement = checkout.createPaymentElement();
|
|
155
162
|
paymentElement.mount("#payment-element");
|
|
163
|
+
} else {
|
|
164
|
+
const errorContent = clientSecretResponse.body.error;
|
|
165
|
+
iap.stripe.error = new Error(errorContent);
|
|
166
|
+
messageText.value = errorContent;
|
|
156
167
|
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
iap.stripe.error = error;
|
|
170
|
+
console.error("Error initializing checkout:", error);
|
|
171
|
+
showPaymentMessage("Failed to initialize payment form");
|
|
157
172
|
}
|
|
158
|
-
} catch (error) {
|
|
159
|
-
console.error("Error initializing checkout:", error);
|
|
160
|
-
showPaymentMessage("Failed to initialize payment form");
|
|
161
173
|
}
|
|
162
|
-
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function showPaymentMessage(messageContent: string) {
|
|
177
|
+
messageText.value = messageContent;
|
|
178
|
+
|
|
179
|
+
setTimeout(() => {
|
|
180
|
+
messageText.value = "";
|
|
181
|
+
}, 8000);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function setLoading(loading: boolean) {
|
|
185
|
+
isLoading.value = loading;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function handleSubmit() {
|
|
189
|
+
console.log("Submitting payment");
|
|
190
|
+
setLoading(true);
|
|
191
|
+
emits("start");
|
|
192
|
+
|
|
193
|
+
if (loadActionsResult?.type === "success") {
|
|
194
|
+
const result = await loadActionsResult.actions.confirm({
|
|
195
|
+
email: props.email,
|
|
196
|
+
returnUrl:
|
|
197
|
+
props.returnUrl + "?stripe_session_id=" + sessionResponse.value.id,
|
|
198
|
+
});
|
|
199
|
+
if (result.type === "error") {
|
|
200
|
+
const errorContent = `${result.error.code}: ${result.error.message}`;
|
|
201
|
+
emits("end", errorContent);
|
|
202
|
+
showPaymentMessage(errorContent);
|
|
203
|
+
} else {
|
|
204
|
+
// TODO: verify with server
|
|
205
|
+
// TODO: connect purchaseItem with a promise resolve function
|
|
206
|
+
emits("end", "");
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
emits("end", "");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
setLoading(false);
|
|
213
|
+
console.log("done Submitting payment");
|
|
214
|
+
}
|
|
163
215
|
</script>
|
|
164
216
|
|
|
165
217
|
<style scoped>
|
|
166
218
|
#payment-form {
|
|
167
|
-
min-width:
|
|
168
|
-
|
|
219
|
+
min-width: 300px;
|
|
220
|
+
width: 600px;
|
|
221
|
+
flex: 1 1 600px;
|
|
169
222
|
}
|
|
170
223
|
.cart {
|
|
171
224
|
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
|
+
}
|