@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.
@@ -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,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
- const props = defineProps<{
73
- cart: StripeCart;
74
- returnUrl: string;
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
- 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
- }
113
+ watch(
114
+ () => props.cart,
115
+ () => {
116
+ getSession();
116
117
  }
117
-
118
- setLoading(false);
119
- };
118
+ );
120
119
 
121
120
  onMounted(async () => {
122
- try {
123
- if (iap.stripe) {
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
- const paymentElement = checkout.createPaymentElement();
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: 400px;
168
- max-width: 600px;
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.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.4",
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
+ }