@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.
@@ -1,19 +1,33 @@
1
1
  <template>
2
- <div v-if="!sessionResponse" id="payment-form p-4">Loading...</div>
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-row flex-wrap gap-2"
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
- <div
10
- v-if="showMessage"
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
- </div>
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="button">
52
- <div class="flex-row justify-center p-2">
53
- <button id="submit" :disabled="isLoading" type="submit">
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
- </button>
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
- const showPaymentMessage = (messageContent: string) => {
88
- messageText.value = messageContent;
89
- showMessage.value = true;
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
- try {
123
- if (iap.stripe) {
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
- const paymentElement = checkout.createPaymentElement();
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: 400px;
168
- max-width: 600px;
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.stripeStatus && iap.stripeCart"
4
- :visible="!!iap.stripeCart"
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.stripeStatus === 'verified' && iap.stripeCart"
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.stripeCart.items" class="flex-row">
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 v-else :cart="iap.stripeCart" :return-url="returnUrl">
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.stripeCallback) {
84
- iap.stripeCallback(undefined);
61
+ if (iap.stripe.callback) {
62
+ iap.stripe.callback(undefined);
85
63
  }
86
- iap.stripeCallback = undefined;
87
- iap.stripeResult = undefined;
88
- iap.stripeCart = undefined;
89
- iap.stripeStatus = undefined;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virgodev/iap",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "main": "./index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
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.warn("CdvPurchase not found, skipping ios/android payments");
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
- STRIP_KEY,
282
- stripe,
283
- stripeLoaded,
284
- stripeCart,
285
- stripeStatus,
286
- stripeResult,
287
- stripeCallback,
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
+ }