@virgodev/iap 1.0.0 → 1.0.1
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 +96 -0
- package/main.ts +4 -0
- package/package.json +6 -3
- package/stores/iap.ts +88 -23
|
@@ -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 "../main";
|
|
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,96 @@
|
|
|
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 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
|
+
onMounted(() => {});
|
|
78
|
+
|
|
79
|
+
function closeDialog(v: boolean): void {
|
|
80
|
+
if (v === false) {
|
|
81
|
+
iap.stripeCart = undefined;
|
|
82
|
+
iap.stripeStatus = undefined;
|
|
83
|
+
visible.value = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<style scoped>
|
|
89
|
+
.complete {
|
|
90
|
+
min-width: 400px;
|
|
91
|
+
max-width: 600px;
|
|
92
|
+
}
|
|
93
|
+
.pi {
|
|
94
|
+
font-size: 3em;
|
|
95
|
+
}
|
|
96
|
+
</style>
|
package/main.ts
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@virgodev/iap",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"main": "./main.ts",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
"license": "ISC",
|
|
10
10
|
"description": "",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@
|
|
13
|
-
"@virgodev/bazaar": "^1.2.7",
|
|
12
|
+
"@stripe/stripe-js": "^8.2.0",
|
|
14
13
|
"cordova-plugin-purchase": "^13.12.1"
|
|
15
14
|
},
|
|
16
15
|
"peerDependencies": {
|
|
16
|
+
"@virgodev/bazaar": "^1.2.7",
|
|
17
|
+
"@capacitor/app": "^7.1.0",
|
|
17
18
|
"@capacitor/device": "^7.0.2",
|
|
19
|
+
"vue-router": "^4.6.3",
|
|
20
|
+
"primevue": "^4.4.1",
|
|
18
21
|
"pinia": "^3.0.3"
|
|
19
22
|
}
|
|
20
23
|
}
|
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 } 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,13 @@ 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">();
|
|
101
128
|
|
|
102
129
|
const storePlatform = computed(() => {
|
|
103
130
|
if (platform.value === "android") {
|
|
@@ -107,14 +134,23 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
107
134
|
}
|
|
108
135
|
});
|
|
109
136
|
|
|
110
|
-
async function initialize(
|
|
137
|
+
async function initialize(loadProducts: IapProduct[]) {
|
|
138
|
+
products.value = loadProducts;
|
|
139
|
+
|
|
140
|
+
if (STRIP_KEY) {
|
|
141
|
+
stripe.value = await loadStripe(STRIP_KEY);
|
|
142
|
+
stripeLoaded.value = true;
|
|
143
|
+
} else {
|
|
144
|
+
console.warn("No STRIPE_KEY provided, skipping in stripe payments");
|
|
145
|
+
}
|
|
146
|
+
|
|
111
147
|
if (window.CdvPurchase) {
|
|
112
148
|
appInfo.value = await App.getInfo();
|
|
113
149
|
deviceIndentifier.value = (await Device.getId()).identifier;
|
|
114
150
|
|
|
115
151
|
const { store, LogLevel } = window.CdvPurchase;
|
|
116
152
|
store.verbosity = LogLevel.INFO;
|
|
117
|
-
|
|
153
|
+
loadProducts.forEach((product) => {
|
|
118
154
|
const p = store.register(product);
|
|
119
155
|
});
|
|
120
156
|
store.off(purchaseCompleteHook);
|
|
@@ -126,22 +162,36 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
126
162
|
|
|
127
163
|
store.initialize();
|
|
128
164
|
} else {
|
|
129
|
-
console.warn("CdvPurchase not found, skipping
|
|
165
|
+
console.warn("CdvPurchase not found, skipping ios/android payments");
|
|
130
166
|
}
|
|
131
167
|
}
|
|
132
168
|
|
|
133
169
|
async function purchaseItem(code: string) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
170
|
+
const product = products.value.find((p) => p.id === code);
|
|
171
|
+
if (product) {
|
|
172
|
+
if (product.platform === Platform.STRIPE) {
|
|
173
|
+
stripeStatus.value = "cart";
|
|
174
|
+
stripeCart.value = {
|
|
175
|
+
items: [{ product, price: product.stripePrice, quantity: 1 }],
|
|
176
|
+
};
|
|
177
|
+
// this should trigger the <StripePopup /> component
|
|
178
|
+
} else if (
|
|
179
|
+
product.platform === Platform.GOOGLE_PLAY ||
|
|
180
|
+
product.platform === Platform.APPLE_APPSTORE
|
|
181
|
+
) {
|
|
182
|
+
if (window.CdvPurchase) {
|
|
183
|
+
const { store } = window.CdvPurchase;
|
|
184
|
+
const product = store.get(code, storePlatform.value);
|
|
185
|
+
if (product) {
|
|
186
|
+
const offer = product.getOffer();
|
|
187
|
+
return offer.order();
|
|
188
|
+
} else {
|
|
189
|
+
console.error("Product not found");
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
throw new Error("CdvPurchase not available");
|
|
193
|
+
}
|
|
142
194
|
}
|
|
143
|
-
} else {
|
|
144
|
-
throw new Error("CdvPurchase not available");
|
|
145
195
|
}
|
|
146
196
|
}
|
|
147
197
|
|
|
@@ -157,16 +207,24 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
157
207
|
return;
|
|
158
208
|
}
|
|
159
209
|
|
|
160
|
-
let productId = p.products[0].id;
|
|
161
|
-
|
|
162
210
|
const finishing = localStorage.getItem("iap:finishing");
|
|
163
211
|
if (finishing !== p.transactionId) {
|
|
164
212
|
localStorage.setItem("iap:finishing", p.transactionId);
|
|
165
213
|
|
|
214
|
+
if (await verify(p)) {
|
|
215
|
+
p.finish();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function verify(p: Transaction) {
|
|
221
|
+
let productId = p.products[0].id;
|
|
222
|
+
const product = products.value.find((p) => p.id === productId);
|
|
223
|
+
if (product) {
|
|
166
224
|
const sub = [
|
|
167
225
|
ProductType.PAID_SUBSCRIPTION,
|
|
168
226
|
ProductType.FREE_SUBSCRIPTION,
|
|
169
|
-
].includes(
|
|
227
|
+
].includes(product.type);
|
|
170
228
|
|
|
171
229
|
const response = await api({
|
|
172
230
|
url: "purchases/verify/",
|
|
@@ -185,13 +243,13 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
185
243
|
},
|
|
186
244
|
});
|
|
187
245
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
246
|
+
return !response.body.error;
|
|
247
|
+
} else {
|
|
248
|
+
throw new Error(`Product not found ${productId}`);
|
|
191
249
|
}
|
|
192
250
|
}
|
|
193
251
|
|
|
194
|
-
function
|
|
252
|
+
function getCdvProducts(): any[] {
|
|
195
253
|
const { store } = window.CdvPurchase;
|
|
196
254
|
return store.products;
|
|
197
255
|
}
|
|
@@ -200,10 +258,17 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
200
258
|
platform,
|
|
201
259
|
storePlatform,
|
|
202
260
|
initialize,
|
|
203
|
-
|
|
261
|
+
getCdvProducts,
|
|
204
262
|
purchaseItem,
|
|
205
263
|
findPurchases,
|
|
264
|
+
verify,
|
|
206
265
|
purchases,
|
|
266
|
+
|
|
267
|
+
STRIP_KEY,
|
|
268
|
+
stripe,
|
|
269
|
+
stripeLoaded,
|
|
270
|
+
stripeCart,
|
|
271
|
+
stripeStatus,
|
|
207
272
|
};
|
|
208
273
|
});
|
|
209
274
|
|