@virgodev/iap 1.0.0 → 1.0.2

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.
@@ -0,0 +1,173 @@
1
+ <template>
2
+ <div v-if="!sessionResponse" id="payment-form p-4">Loading...</div>
3
+ <form
4
+ v-else
5
+ id="payment-form"
6
+ class="p-3-t flex-row flex-wrap gap-2"
7
+ @submit.prevent="handleSubmit"
8
+ >
9
+ <div
10
+ v-if="showMessage"
11
+ id="payment-message"
12
+ :class="{ hidden: !showMessage }"
13
+ class="message"
14
+ >
15
+ {{ messageText }}
16
+ </div>
17
+
18
+ <div class="cart flex-column flex-stretch p-1">
19
+ <div v-for="item in cart.items" :key="item.id" class="flex-column p-1">
20
+ <div class="flex-row align-center p-1">
21
+ <div class="font-size-2 flex-stretch">{{ item.product.name }}</div>
22
+ <div>x{{ item.quantity }}</div>
23
+ </div>
24
+ <div v-if="item.product.description" class="p-1-x p-1-t">
25
+ <i>{{ item.product.description }}</i>
26
+ </div>
27
+ </div>
28
+ <div class="flex-row p-2">
29
+ <div class="flex-stretch">Total:</div>
30
+ <div>
31
+ {{ sessionResponse.total }} USD
32
+ <span
33
+ v-if="
34
+ cart.items.some(
35
+ (i) => i.product.type === ProductType.PAID_SUBSCRIPTION
36
+ )
37
+ "
38
+ >
39
+ / Month
40
+ </span>
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="payments flex-stretch">
46
+ <div id="payment-element">
47
+ Loading...
48
+ <!-- Stripe.js injects the Payment Element -->
49
+ </div>
50
+
51
+ <slot name="button">
52
+ <div class="flex-row justify-center p-2">
53
+ <button id="submit" :disabled="isLoading" type="submit">
54
+ Pay Now
55
+ </button>
56
+ </div>
57
+ </slot>
58
+ </div>
59
+ </form>
60
+ </template>
61
+
62
+ <script setup lang="ts">
63
+ import type {
64
+ StripeCheckout,
65
+ StripeCheckoutLoadActionsResult,
66
+ } from "@stripe/stripe-js";
67
+ import api from "@virgodev/bazaar/functions/api";
68
+ import { onMounted, ref } from "vue";
69
+ import { ProductType, useIapStore } from "../index";
70
+ import type { StripeCart } from "../stores/iap";
71
+
72
+ const props = defineProps<{
73
+ cart: StripeCart;
74
+ returnUrl: string;
75
+ }>();
76
+
77
+ const iap = useIapStore();
78
+
79
+ const isLoading = ref<boolean>(false);
80
+ const showMessage = ref(false);
81
+ const messageText = ref("");
82
+ const sessionResponse = ref<any>();
83
+
84
+ let checkout: StripeCheckout | null = null;
85
+ let loadActionsResult: StripeCheckoutLoadActionsResult | null = null;
86
+
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
+ }
116
+ }
117
+
118
+ setLoading(false);
119
+ };
120
+
121
+ onMounted(async () => {
122
+ try {
123
+ if (iap.stripe) {
124
+ // Fetch client secret from backend
125
+ const clientSecretResponse = await api({
126
+ url: "purchases/session/",
127
+ method: "POST",
128
+ json: {
129
+ items: props.cart.items,
130
+ sub: props.cart.items.some(
131
+ (i) => i.product.type === ProductType.PAID_SUBSCRIPTION
132
+ ),
133
+ },
134
+ });
135
+ if (clientSecretResponse.ok) {
136
+ sessionResponse.value = clientSecretResponse.body;
137
+
138
+ checkout = await iap.stripe.initCheckout({
139
+ clientSecret: sessionResponse.value.client_secret,
140
+ elementsOptions: { appearance: { theme: "stripe" } },
141
+ });
142
+
143
+ checkout.on("change", (session: any) => {
144
+ // Handle changes to the checkout session
145
+ console.log("session changed:", session);
146
+ });
147
+
148
+ loadActionsResult = await checkout.loadActions();
149
+
150
+ if (loadActionsResult.type !== "success") {
151
+ showPaymentMessage("Failed to load payment actions");
152
+ }
153
+
154
+ const paymentElement = checkout.createPaymentElement();
155
+ paymentElement.mount("#payment-element");
156
+ }
157
+ }
158
+ } catch (error) {
159
+ console.error("Error initializing checkout:", error);
160
+ showPaymentMessage("Failed to initialize payment form");
161
+ }
162
+ });
163
+ </script>
164
+
165
+ <style scoped>
166
+ #payment-form {
167
+ min-width: 400px;
168
+ max-width: 600px;
169
+ }
170
+ .cart {
171
+ min-width: 250px;
172
+ }
173
+ </style>
@@ -0,0 +1,227 @@
1
+ <template>
2
+ <div id="payment-status">
3
+ <div id="status-icon" :style="{ backgroundColor: iconColor }">
4
+ <svg
5
+ v-if="icon === 'success'"
6
+ width="16"
7
+ height="14"
8
+ viewBox="0 0 16 14"
9
+ fill="none"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ >
12
+ <path
13
+ fill-rule="evenodd"
14
+ clip-rule="evenodd"
15
+ d="M15.4695 0.232963C15.8241 0.561287 15.8454 1.1149 15.5171 1.46949L6.14206 11.5945C5.97228 11.7778 5.73221 11.8799 5.48237 11.8748C5.23253 11.8698 4.99677 11.7582 4.83452 11.5681L0.459523 6.44311C0.145767 6.07557 0.18937 5.52327 0.556912 5.20951C0.924454 4.89575 1.47676 4.93936 1.79051 5.3069L5.52658 9.68343L14.233 0.280522C14.5613 -0.0740672 15.1149 -0.0953599 15.4695 0.232963Z"
16
+ fill="white"
17
+ />
18
+ </svg>
19
+ <svg
20
+ v-else
21
+ width="16"
22
+ height="16"
23
+ viewBox="0 0 16 16"
24
+ fill="none"
25
+ xmlns="http://www.w3.org/2000/svg"
26
+ >
27
+ <path
28
+ fill-rule="evenodd"
29
+ clip-rule="evenodd"
30
+ d="M1.25628 1.25628C1.59799 0.914573 2.15201 0.914573 2.49372 1.25628L8 6.76256L13.5063 1.25628C13.848 0.914573 14.402 0.914573 14.7437 1.25628C15.0854 1.59799 15.0854 2.15201 14.7437 2.49372L9.23744 8L14.7437 13.5063C15.0854 13.848 15.0854 14.402 14.7437 14.7437C14.402 15.0854 13.848 15.0854 13.5063 14.7437L8 9.23744L2.49372 14.7437C2.15201 15.0854 1.59799 15.0854 1.25628 14.7437C0.914573 14.402 0.914573 13.848 1.25628 13.5063L6.76256 8L1.25628 2.49372C0.914573 2.15201 0.914573 1.59799 1.25628 1.25628Z"
31
+ fill="white"
32
+ />
33
+ </svg>
34
+ </div>
35
+ <h2 id="status-text">{{ text }}</h2>
36
+ <div id="details-table">
37
+ <table>
38
+ <tbody>
39
+ <tr>
40
+ <td class="table-label">Payment Intent ID</td>
41
+ <td id="intent-id" class="table-content">{{ paymentIntentId }}</td>
42
+ </tr>
43
+ <tr>
44
+ <td class="table-label">Status</td>
45
+ <td id="intent-status" class="table-content">{{ status }}</td>
46
+ </tr>
47
+ <tr>
48
+ <td class="table-label">Payment Status</td>
49
+ <td id="session-status" class="table-content">
50
+ {{ paymentStatus }}
51
+ </td>
52
+ </tr>
53
+ <tr>
54
+ <td class="table-label">Payment Intent Status</td>
55
+ <td id="payment-intent-status" class="table-content">
56
+ {{ paymentIntentStatus }}
57
+ </td>
58
+ </tr>
59
+ </tbody>
60
+ </table>
61
+ </div>
62
+ <a
63
+ :href="`https://dashboard.stripe.com/payments/${paymentIntentId}`"
64
+ id="view-details"
65
+ rel="noopener noreferrer"
66
+ target="_blank"
67
+ >
68
+ View details
69
+ <svg
70
+ width="15"
71
+ height="14"
72
+ viewBox="0 0 15 14"
73
+ fill="none"
74
+ xmlns="http://www.w3.org/2000/svg"
75
+ >
76
+ <path
77
+ fill-rule="evenodd"
78
+ clip-rule="evenodd"
79
+ d="M3.125 3.49998C2.64175 3.49998 2.25 3.89173 2.25 4.37498V11.375C2.25 11.8582 2.64175 12.25 3.125 12.25H10.125C10.6082 12.25 11 11.8582 11 11.375V9.62498C11 9.14173 11.3918 8.74998 11.875 8.74998C12.3582 8.74998 12.75 9.14173 12.75 9.62498V11.375C12.75 12.8247 11.5747 14 10.125 14H3.125C1.67525 14 0.5 12.8247 0.5 11.375V4.37498C0.5 2.92524 1.67525 1.74998 3.125 1.74998H4.875C5.35825 1.74998 5.75 2.14173 5.75 2.62498C5.75 3.10823 5.35825 3.49998 4.875 3.49998H3.125Z"
80
+ fill="#0055DE"
81
+ />
82
+ <path
83
+ d="M8.66672 0C8.18347 0 7.79172 0.391751 7.79172 0.875C7.79172 1.35825 8.18347 1.75 8.66672 1.75H11.5126L4.83967 8.42295C4.49796 8.76466 4.49796 9.31868 4.83967 9.66039C5.18138 10.0021 5.7354 10.0021 6.07711 9.66039L12.7501 2.98744V5.83333C12.7501 6.31658 13.1418 6.70833 13.6251 6.70833C14.1083 6.70833 14.5001 6.31658 14.5001 5.83333V0.875C14.5001 0.391751 14.1083 0 13.6251 0H8.66672Z"
84
+ fill="#0055DE"
85
+ />
86
+ </svg>
87
+ </a>
88
+ <RouterLink id="retry-button" to="/checkout">Test another</RouterLink>
89
+ </div>
90
+ </template>
91
+
92
+ <script setup lang="ts">
93
+ import { ref, onMounted } from "vue";
94
+ import { useRoute } from "vue-router";
95
+
96
+ const route = useRoute();
97
+
98
+ const status = ref<string | null>(null);
99
+ const paymentIntentId = ref("");
100
+ const paymentStatus = ref("");
101
+ const paymentIntentStatus = ref("");
102
+ const iconColor = ref("");
103
+ const icon = ref<"success" | "error">("error");
104
+ const text = ref("");
105
+
106
+ onMounted(async () => {
107
+ const queryString = window.location.search;
108
+ const urlParams = new URLSearchParams(queryString);
109
+ const sessionId = urlParams.get("session_id");
110
+
111
+ try {
112
+ const response = await fetch(`/session-status?session_id=${sessionId}`);
113
+ const data = await response.json();
114
+
115
+ status.value = data.status;
116
+ paymentIntentId.value = data.payment_intent_id;
117
+ paymentStatus.value = data.payment_status;
118
+ paymentIntentStatus.value = data.payment_intent_status;
119
+
120
+ if (data.status === "complete") {
121
+ iconColor.value = "#30B130";
122
+ icon.value = "success";
123
+ text.value = "Payment succeeded";
124
+ } else {
125
+ iconColor.value = "#DF1B41";
126
+ icon.value = "error";
127
+ text.value = "Something went wrong, please try again.";
128
+ }
129
+ } catch (error) {
130
+ console.error("Error fetching session status:", error);
131
+ iconColor.value = "#DF1B41";
132
+ icon.value = "error";
133
+ text.value = "Something went wrong, please try again.";
134
+ }
135
+ });
136
+ </script>
137
+
138
+ <style scoped>
139
+ #payment-status {
140
+ display: flex;
141
+ flex-direction: column;
142
+ align-items: center;
143
+ gap: 20px;
144
+ padding: 40px 20px;
145
+ }
146
+
147
+ #status-icon {
148
+ width: 60px;
149
+ height: 60px;
150
+ border-radius: 50%;
151
+ display: flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ }
155
+
156
+ #status-icon svg {
157
+ width: 100%;
158
+ height: 100%;
159
+ }
160
+
161
+ #status-text {
162
+ font-size: 24px;
163
+ font-weight: 600;
164
+ margin: 0;
165
+ color: #333;
166
+ }
167
+
168
+ #details-table {
169
+ width: 100%;
170
+ max-width: 600px;
171
+ margin: 20px 0;
172
+ }
173
+
174
+ table {
175
+ width: 100%;
176
+ border-collapse: collapse;
177
+ }
178
+
179
+ tr {
180
+ border-bottom: 1px solid #e0e0e0;
181
+ }
182
+
183
+ .table-label {
184
+ font-weight: 600;
185
+ padding: 12px;
186
+ text-align: left;
187
+ background-color: #f5f5f5;
188
+ width: 40%;
189
+ }
190
+
191
+ .table-content {
192
+ padding: 12px;
193
+ text-align: left;
194
+ word-break: break-all;
195
+ }
196
+
197
+ #view-details,
198
+ #retry-button {
199
+ display: inline-flex;
200
+ align-items: center;
201
+ gap: 8px;
202
+ padding: 12px 24px;
203
+ margin: 10px;
204
+ border-radius: 6px;
205
+ text-decoration: none;
206
+ font-weight: 500;
207
+ transition: all 0.3s ease;
208
+ }
209
+
210
+ #view-details {
211
+ background-color: #f0f0f0;
212
+ color: #0055de;
213
+ }
214
+
215
+ #view-details:hover {
216
+ background-color: #e0e0e0;
217
+ }
218
+
219
+ #retry-button {
220
+ background-color: #0055de;
221
+ color: white;
222
+ }
223
+
224
+ #retry-button:hover {
225
+ background-color: #0044b0;
226
+ }
227
+ </style>
@@ -0,0 +1,103 @@
1
+ <template>
2
+ <Dialog
3
+ v-if="iap.stripeStatus && iap.stripeCart"
4
+ :visible="!!iap.stripeCart"
5
+ :dismissable-mask="true"
6
+ :modal="true"
7
+ :show-header="false"
8
+ @update:visible="closeDialog"
9
+ >
10
+ <div
11
+ v-if="iap.stripeStatus === 'verified' && iap.stripeCart"
12
+ class="complete flex-column justify-center align-items-center p-3"
13
+ >
14
+ <h2 class="text-center p-1">Payment Complete!</h2>
15
+ <i class="pi pi-check-circle text-success p-2 text-center" />
16
+ <div class="cart p-3-x p-1-y">
17
+ <div v-for="item of iap.stripeCart.items" class="flex-row">
18
+ <div class="flex-stretch">
19
+ <b>{{ item.product?.name }}</b>
20
+ </div>
21
+ <div>x{{ item.quantity }}</div>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ <StripeCheckout v-else :cart="iap.stripeCart" :return-url="returnUrl">
26
+ <template #button>
27
+ <div class="flex-row justify-stretch p-1-y">
28
+ <Button type="submit" class="flex-stretch">Pay Now</Button>
29
+ </div>
30
+ </template>
31
+ </StripeCheckout>
32
+ </Dialog>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import Dialog from "primevue/dialog";
37
+ import { computed, nextTick, onMounted, ref } from "vue";
38
+ import { useRouter } from "vue-router";
39
+ import { Platform, useIapStore } from "../stores/iap";
40
+ import StripeCheckout from "./StripeCheckout.vue";
41
+
42
+ const router = useRouter();
43
+ const iap = useIapStore();
44
+ const visible = ref(false);
45
+
46
+ const returnUrl = computed(() => {
47
+ return `${location.protocol}//${location.host}${router.currentRoute.value.fullPath}`;
48
+ });
49
+
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
+ onMounted(() => {});
80
+
81
+ function closeDialog(v: boolean): void {
82
+ if (v === false) {
83
+ if (iap.stripeCallback) {
84
+ iap.stripeCallback(undefined);
85
+ }
86
+ iap.stripeCallback = undefined;
87
+ iap.stripeResult = undefined;
88
+ iap.stripeCart = undefined;
89
+ iap.stripeStatus = undefined;
90
+ visible.value = false;
91
+ }
92
+ }
93
+ </script>
94
+
95
+ <style scoped>
96
+ .complete {
97
+ min-width: 400px;
98
+ max-width: 600px;
99
+ }
100
+ .pi {
101
+ font-size: 3em;
102
+ }
103
+ </style>
package/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { Platform, ProductType, useIapStore } from "./stores/iap";
2
+
3
+ export { default as StripeCheckout } from "./components/StripeCheckout.vue";
4
+ export { default as StripeComplete } from "./components/StripeComplete.vue";
5
+ export { default as StripePopup } from "./components/StripePopup.vue";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@virgodev/iap",
3
- "version": "1.0.0",
4
- "main": "./main.ts",
3
+ "version": "1.0.2",
4
+ "main": "./index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
7
7
  },
@@ -9,12 +9,16 @@
9
9
  "license": "ISC",
10
10
  "description": "",
11
11
  "dependencies": {
12
- "@capacitor/app": "^7.1.0",
13
- "@virgodev/bazaar": "^1.2.7",
12
+ "@stripe/stripe-js": "^8.2.0",
13
+ "@virgodev/iap": "^1.0.1",
14
14
  "cordova-plugin-purchase": "^13.12.1"
15
15
  },
16
16
  "peerDependencies": {
17
+ "@capacitor/app": "^7.1.0",
17
18
  "@capacitor/device": "^7.0.2",
18
- "pinia": "^3.0.3"
19
+ "@virgodev/bazaar": "^1.2.7",
20
+ "pinia": "^3.0.3",
21
+ "primevue": "^4.4.1",
22
+ "vue-router": "^4.6.3"
19
23
  }
20
24
  }
package/stores/iap.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { App, type AppInfo } from "@capacitor/app";
2
2
  import { Capacitor } from "@capacitor/core";
3
3
  import { Device } from "@capacitor/device";
4
+ import { loadStripe, type Stripe } from "@stripe/stripe-js";
4
5
  import api from "@virgodev/bazaar/functions/api";
6
+ import { localRef } from "@virgodev/bazaar/functions/localstorage/localRef";
5
7
  import { createStore } from "@virgodev/vue-models/utils/create_store";
6
8
  import { defineStore } from "pinia";
7
- import { computed, ref } from "vue";
9
+ import { computed, ref, shallowRef, watch } from "vue";
8
10
  import { useRouter } from "vue-router";
9
11
 
10
12
  export enum ProductType {
@@ -21,15 +23,29 @@ export enum Platform {
21
23
  GOOGLE_PLAY = "android-playstore",
22
24
  WINDOWS_STORE = "windows-store-transaction",
23
25
  BRAINTREE = "braintree",
26
+ STRIPE = "stripe",
24
27
  TEST = "test",
25
28
  }
26
29
 
27
- export interface IapProduct {
30
+ export interface BaseIapProduct {
28
31
  id: string;
32
+ name: string;
33
+ description?: string;
29
34
  type: ProductType;
30
35
  platform: Platform;
31
36
  }
32
37
 
38
+ export interface MobileIapProduct extends BaseIapProduct {
39
+ platform: Platform.APPLE_APPSTORE | Platform.GOOGLE_PLAY;
40
+ }
41
+
42
+ export interface StripeIapProduct extends BaseIapProduct {
43
+ platform: Platform.STRIPE;
44
+ stripePrice: string;
45
+ }
46
+
47
+ export type IapProduct = MobileIapProduct | StripeIapProduct;
48
+
33
49
  export interface Transaction {
34
50
  // "className": "Transaction",
35
51
  // "transactionId": "GPA.3336-6862-5718-29728",
@@ -89,6 +105,10 @@ export interface Purchase {
89
105
  error: string;
90
106
  }
91
107
 
108
+ export interface StripeCart {
109
+ items: { product: IapProduct; price: string; quantity: number }[];
110
+ }
111
+
92
112
  export const usePurchases = createStore<Purchase>("purchases");
93
113
 
94
114
  export const useIapStore = defineStore("iap", () => {
@@ -98,6 +118,16 @@ export const useIapStore = defineStore("iap", () => {
98
118
  const purchases = usePurchases();
99
119
  const appInfo = ref<AppInfo>();
100
120
  const deviceIndentifier = ref("");
121
+ const products = ref<IapProduct[]>([]);
122
+
123
+ const STRIP_KEY = import.meta.env.VITE_APP_STRIPE;
124
+ const stripe = shallowRef<Stripe | null>(null);
125
+ const stripeLoaded = ref(false);
126
+ const stripeCart = localRef<StripeCart | undefined>("stripe-cart", undefined);
127
+ const stripeStatus = ref<undefined | "cart" | "verified">();
128
+ const stripeResult = ref<undefined | Transaction>();
129
+ const stripeCallback =
130
+ ref<(t: Transaction | undefined) => Transaction | undefined>();
101
131
 
102
132
  const storePlatform = computed(() => {
103
133
  if (platform.value === "android") {
@@ -107,14 +137,31 @@ export const useIapStore = defineStore("iap", () => {
107
137
  }
108
138
  });
109
139
 
110
- async function initialize(products: IapProduct[]) {
140
+ watch(stripeResult, () => {
141
+ if (stripeCallback.value) {
142
+ stripeCallback.value(stripeResult.value);
143
+ stripeCallback.value = undefined;
144
+ stripeResult.value = undefined;
145
+ }
146
+ });
147
+
148
+ async function initialize(loadProducts: IapProduct[]) {
149
+ products.value = loadProducts;
150
+
151
+ if (STRIP_KEY) {
152
+ stripe.value = await loadStripe(STRIP_KEY);
153
+ stripeLoaded.value = true;
154
+ } else {
155
+ console.warn("No STRIPE_KEY provided, skipping in stripe payments");
156
+ }
157
+
111
158
  if (window.CdvPurchase) {
112
159
  appInfo.value = await App.getInfo();
113
160
  deviceIndentifier.value = (await Device.getId()).identifier;
114
161
 
115
162
  const { store, LogLevel } = window.CdvPurchase;
116
163
  store.verbosity = LogLevel.INFO;
117
- products.forEach((product) => {
164
+ loadProducts.forEach((product) => {
118
165
  const p = store.register(product);
119
166
  });
120
167
  store.off(purchaseCompleteHook);
@@ -126,22 +173,39 @@ export const useIapStore = defineStore("iap", () => {
126
173
 
127
174
  store.initialize();
128
175
  } else {
129
- console.warn("CdvPurchase not found, skipping in app purchases");
176
+ console.warn("CdvPurchase not found, skipping ios/android payments");
130
177
  }
131
178
  }
132
179
 
133
- async function purchaseItem(code: string) {
134
- if (window.CdvPurchase) {
135
- const { store } = window.CdvPurchase;
136
- const product = store.get(code, storePlatform.value);
137
- if (product) {
138
- const offer = product.getOffer();
139
- return offer.order();
140
- } else {
141
- console.error("Product not found");
180
+ async function purchaseItem(code: string): Promise<any | undefined> {
181
+ const product = products.value.find((p) => p.id === code);
182
+ if (product) {
183
+ if (product.platform === Platform.STRIPE) {
184
+ stripeStatus.value = "cart";
185
+ stripeCart.value = {
186
+ items: [{ product, price: product.stripePrice, quantity: 1 }],
187
+ };
188
+ return await new Promise<Transaction | undefined>((resolve) => {
189
+ stripeCallback.value = resolve;
190
+ });
191
+ // this should trigger the <StripePopup /> component
192
+ } else if (
193
+ product.platform === Platform.GOOGLE_PLAY ||
194
+ product.platform === Platform.APPLE_APPSTORE
195
+ ) {
196
+ if (window.CdvPurchase) {
197
+ const { store } = window.CdvPurchase;
198
+ const product = store.get(code, storePlatform.value);
199
+ if (product) {
200
+ const offer = product.getOffer();
201
+ return offer.order();
202
+ } else {
203
+ console.error("Product not found");
204
+ }
205
+ } else {
206
+ throw new Error("CdvPurchase not available");
207
+ }
142
208
  }
143
- } else {
144
- throw new Error("CdvPurchase not available");
145
209
  }
146
210
  }
147
211
 
@@ -157,16 +221,24 @@ export const useIapStore = defineStore("iap", () => {
157
221
  return;
158
222
  }
159
223
 
160
- let productId = p.products[0].id;
161
-
162
224
  const finishing = localStorage.getItem("iap:finishing");
163
225
  if (finishing !== p.transactionId) {
164
226
  localStorage.setItem("iap:finishing", p.transactionId);
165
227
 
228
+ if (await verify(p)) {
229
+ p.finish();
230
+ }
231
+ }
232
+ }
233
+
234
+ async function verify(p: Transaction) {
235
+ let productId = p.products[0].id;
236
+ const product = products.value.find((p) => p.id === productId);
237
+ if (product) {
166
238
  const sub = [
167
239
  ProductType.PAID_SUBSCRIPTION,
168
240
  ProductType.FREE_SUBSCRIPTION,
169
- ].includes(store.products.find((p2: any) => p2.id === productId)?.type);
241
+ ].includes(product.type);
170
242
 
171
243
  const response = await api({
172
244
  url: "purchases/verify/",
@@ -185,13 +257,13 @@ export const useIapStore = defineStore("iap", () => {
185
257
  },
186
258
  });
187
259
 
188
- if (!response.body.error) {
189
- p.finish();
190
- }
260
+ return !response.body.error;
261
+ } else {
262
+ throw new Error(`Product not found ${productId}`);
191
263
  }
192
264
  }
193
265
 
194
- function getProducts(): any[] {
266
+ function getCdvProducts(): any[] {
195
267
  const { store } = window.CdvPurchase;
196
268
  return store.products;
197
269
  }
@@ -200,10 +272,19 @@ export const useIapStore = defineStore("iap", () => {
200
272
  platform,
201
273
  storePlatform,
202
274
  initialize,
203
- getProducts,
275
+ getCdvProducts,
204
276
  purchaseItem,
205
277
  findPurchases,
278
+ verify,
206
279
  purchases,
280
+
281
+ STRIP_KEY,
282
+ stripe,
283
+ stripeLoaded,
284
+ stripeCart,
285
+ stripeStatus,
286
+ stripeResult,
287
+ stripeCallback,
207
288
  };
208
289
  });
209
290
 
package/main.ts DELETED
@@ -1 +0,0 @@
1
- export { Platform, ProductType, useIapStore } from "./stores/iap";