@virgodev/iap 1.0.1 → 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";
69
- import { ProductType, useIapStore } from "../main";
82
+ import { onMounted, ref, watch, nextTick } from "vue";
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,43 +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 result = await iap.verify({
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
- if (result) {
64
- iap.stripeStatus = "verified";
65
- delete to.query.stripe_session_id;
66
- }
67
- next(to);
68
- } else {
69
- console.warn("Found stripe session, but cart is empty");
70
- next();
71
- }
72
- } else {
73
- next();
74
- }
75
- });
76
-
77
57
  onMounted(() => {});
78
58
 
79
59
  function closeDialog(v: boolean): void {
80
60
  if (v === false) {
81
- iap.stripeCart = undefined;
82
- iap.stripeStatus = undefined;
61
+ if (iap.stripe.callback) {
62
+ iap.stripe.callback(undefined);
63
+ }
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;
83
69
  visible.value = false;
84
70
  }
85
71
  }
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "@virgodev/iap",
3
- "version": "1.0.1",
4
- "main": "./main.ts",
3
+ "version": "1.0.3",
4
+ "main": "./index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
7
7
  },
@@ -10,14 +10,15 @@
10
10
  "description": "",
11
11
  "dependencies": {
12
12
  "@stripe/stripe-js": "^8.2.0",
13
+ "@virgodev/iap": "^1.0.1",
13
14
  "cordova-plugin-purchase": "^13.12.1"
14
15
  },
15
16
  "peerDependencies": {
16
- "@virgodev/bazaar": "^1.2.7",
17
17
  "@capacitor/app": "^7.1.0",
18
18
  "@capacitor/device": "^7.0.2",
19
- "vue-router": "^4.6.3",
19
+ "@virgodev/bazaar": "^1.2.7",
20
+ "pinia": "^3.0.3",
20
21
  "primevue": "^4.4.1",
21
- "pinia": "^3.0.3"
22
+ "vue-router": "^4.6.3"
22
23
  }
23
24
  }
package/stores/iap.ts CHANGED
@@ -6,7 +6,7 @@ import api from "@virgodev/bazaar/functions/api";
6
6
  import { localRef } from "@virgodev/bazaar/functions/localstorage/localRef";
7
7
  import { createStore } from "@virgodev/vue-models/utils/create_store";
8
8
  import { defineStore } from "pinia";
9
- import { computed, ref, shallowRef } from "vue";
9
+ import { computed, ref, shallowRef, watch } from "vue";
10
10
  import { useRouter } from "vue-router";
11
11
 
12
12
  export enum ProductType {
@@ -125,6 +125,10 @@ export const useIapStore = defineStore("iap", () => {
125
125
  const stripeLoaded = ref(false);
126
126
  const stripeCart = localRef<StripeCart | undefined>("stripe-cart", undefined);
127
127
  const stripeStatus = ref<undefined | "cart" | "verified">();
128
+ const stripeResult = ref<undefined | Transaction>();
129
+ const stripeCallback =
130
+ ref<(t: Transaction | undefined) => Transaction | undefined>();
131
+ const stripeError = ref<undefined | string>();
128
132
 
129
133
  const storePlatform = computed(() => {
130
134
  if (platform.value === "android") {
@@ -134,6 +138,14 @@ export const useIapStore = defineStore("iap", () => {
134
138
  }
135
139
  });
136
140
 
141
+ watch(stripeResult, () => {
142
+ if (stripeCallback.value) {
143
+ stripeCallback.value(stripeResult.value);
144
+ stripeCallback.value = undefined;
145
+ stripeResult.value = undefined;
146
+ }
147
+ });
148
+
137
149
  async function initialize(loadProducts: IapProduct[]) {
138
150
  products.value = loadProducts;
139
151
 
@@ -162,11 +174,12 @@ export const useIapStore = defineStore("iap", () => {
162
174
 
163
175
  store.initialize();
164
176
  } else {
165
- console.warn("CdvPurchase not found, skipping ios/android payments");
177
+ console.debug("CdvPurchase not found, skipping ios/android payments");
178
+ isReady.value = true;
166
179
  }
167
180
  }
168
181
 
169
- async function purchaseItem(code: string) {
182
+ async function purchaseItem(code: string): Promise<any | undefined> {
170
183
  const product = products.value.find((p) => p.id === code);
171
184
  if (product) {
172
185
  if (product.platform === Platform.STRIPE) {
@@ -174,6 +187,9 @@ export const useIapStore = defineStore("iap", () => {
174
187
  stripeCart.value = {
175
188
  items: [{ product, price: product.stripePrice, quantity: 1 }],
176
189
  };
190
+ return await new Promise<Transaction | undefined>((resolve) => {
191
+ stripeCallback.value = resolve;
192
+ });
177
193
  // this should trigger the <StripePopup /> component
178
194
  } else if (
179
195
  product.platform === Platform.GOOGLE_PLAY ||
@@ -218,6 +234,8 @@ export const useIapStore = defineStore("iap", () => {
218
234
  }
219
235
 
220
236
  async function verify(p: Transaction) {
237
+ await ready();
238
+
221
239
  let productId = p.products[0].id;
222
240
  const product = products.value.find((p) => p.id === productId);
223
241
  if (product) {
@@ -254,7 +272,22 @@ export const useIapStore = defineStore("iap", () => {
254
272
  return store.products;
255
273
  }
256
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
+
257
289
  return {
290
+ products,
258
291
  platform,
259
292
  storePlatform,
260
293
  initialize,
@@ -263,12 +296,18 @@ export const useIapStore = defineStore("iap", () => {
263
296
  findPurchases,
264
297
  verify,
265
298
  purchases,
266
-
267
- STRIP_KEY,
268
- stripe,
269
- stripeLoaded,
270
- stripeCart,
271
- stripeStatus,
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
+ },
272
311
  };
273
312
  });
274
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
+ }