@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.
- package/components/StripeCheckout.vue +173 -0
- package/components/StripeComplete.vue +227 -0
- package/components/StripePopup.vue +103 -0
- package/index.ts +5 -0
- package/package.json +9 -5
- package/stores/iap.ts +105 -24
- package/main.ts +0 -1
|
@@ -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.
|
|
4
|
-
"main": "./
|
|
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
|
-
"@
|
|
13
|
-
"@virgodev/
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
176
|
+
console.warn("CdvPurchase not found, skipping ios/android payments");
|
|
130
177
|
}
|
|
131
178
|
}
|
|
132
179
|
|
|
133
|
-
async function purchaseItem(code: string) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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(
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
260
|
+
return !response.body.error;
|
|
261
|
+
} else {
|
|
262
|
+
throw new Error(`Product not found ${productId}`);
|
|
191
263
|
}
|
|
192
264
|
}
|
|
193
265
|
|
|
194
|
-
function
|
|
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
|
-
|
|
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";
|