@virgodev/iap 1.0.5 → 1.0.7
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 +107 -26
- package/package.json +4 -2
- package/stores/iap.ts +28 -2
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
<span
|
|
47
47
|
v-if="
|
|
48
48
|
cart.items.some(
|
|
49
|
-
(i) => i.product.type === ProductType.PAID_SUBSCRIPTION
|
|
49
|
+
(i) => i.product.type === ProductType.PAID_SUBSCRIPTION,
|
|
50
50
|
)
|
|
51
51
|
"
|
|
52
52
|
>
|
|
@@ -54,6 +54,38 @@
|
|
|
54
54
|
</span>
|
|
55
55
|
</div>
|
|
56
56
|
</div>
|
|
57
|
+
<div v-if="showCoupon" class="coupon-row flex-row p-1 gap-1 align-center">
|
|
58
|
+
<template v-if="appliedCoupon">
|
|
59
|
+
<div class="flex-stretch coupon-applied">
|
|
60
|
+
Coupon <b>{{ appliedCoupon.id }}</b
|
|
61
|
+
>:
|
|
62
|
+
{{
|
|
63
|
+
appliedCoupon.percent_off
|
|
64
|
+
? appliedCoupon.percent_off + "% off"
|
|
65
|
+
: "$" + appliedCoupon.amount_off + " off"
|
|
66
|
+
}}
|
|
67
|
+
</div>
|
|
68
|
+
<Button size="small" severity="secondary" @click="removeCoupon"
|
|
69
|
+
>Remove</Button
|
|
70
|
+
>
|
|
71
|
+
</template>
|
|
72
|
+
<template v-else>
|
|
73
|
+
<InputText
|
|
74
|
+
v-model="couponInput"
|
|
75
|
+
placeholder="Coupon code"
|
|
76
|
+
size="small"
|
|
77
|
+
class="flex-stretch"
|
|
78
|
+
:disabled="couponLoading"
|
|
79
|
+
@keyup.enter="applyCoupon"
|
|
80
|
+
/>
|
|
81
|
+
<Button size="small" :loading="couponLoading" @click="applyCoupon">
|
|
82
|
+
Apply
|
|
83
|
+
</Button>
|
|
84
|
+
</template>
|
|
85
|
+
</div>
|
|
86
|
+
<div v-if="couponError" class="p-1-x">
|
|
87
|
+
<Message severity="error" size="small">{{ couponError }}</Message>
|
|
88
|
+
</div>
|
|
57
89
|
</div>
|
|
58
90
|
|
|
59
91
|
<div class="payments flex-stretch">
|
|
@@ -84,15 +116,17 @@ import { ProductType, useIapStore } from "../index";
|
|
|
84
116
|
import type { StripeCart } from "../stores/iap";
|
|
85
117
|
import Message from "primevue/message";
|
|
86
118
|
import Button from "primevue/button";
|
|
119
|
+
import InputText from "primevue/inputtext";
|
|
87
120
|
|
|
88
121
|
const props = withDefaults(
|
|
89
122
|
defineProps<{
|
|
90
123
|
cart: StripeCart;
|
|
91
124
|
returnUrl: string;
|
|
92
125
|
email: string;
|
|
93
|
-
vertical
|
|
126
|
+
vertical?: boolean;
|
|
127
|
+
showCoupon?: boolean;
|
|
94
128
|
}>(),
|
|
95
|
-
{ vertical: false }
|
|
129
|
+
{ vertical: false, showCoupon: true },
|
|
96
130
|
);
|
|
97
131
|
|
|
98
132
|
const emits = defineEmits<{
|
|
@@ -105,17 +139,18 @@ const iap = useIapStore();
|
|
|
105
139
|
const isLoading = ref<boolean>(false);
|
|
106
140
|
const messageText = ref("");
|
|
107
141
|
const sessionResponse: any = ref<any>();
|
|
142
|
+
const couponInput = ref("");
|
|
143
|
+
const couponLoading = ref(false);
|
|
144
|
+
const couponError = ref("");
|
|
145
|
+
const appliedCoupon = ref<any>(null);
|
|
108
146
|
|
|
109
147
|
let checkout: StripeCheckout | null = null;
|
|
110
148
|
let loadActionsResult: StripeCheckoutLoadActionsResult | null = null;
|
|
111
|
-
let paymentElement = undefined;
|
|
149
|
+
let paymentElement: any | undefined = undefined;
|
|
112
150
|
|
|
113
|
-
watch(
|
|
114
|
-
()
|
|
115
|
-
|
|
116
|
-
getSession();
|
|
117
|
-
}
|
|
118
|
-
);
|
|
151
|
+
watch([() => props.cart, () => iap.stripe.coupon], () => {
|
|
152
|
+
getSession();
|
|
153
|
+
});
|
|
119
154
|
|
|
120
155
|
onMounted(async () => {
|
|
121
156
|
getSession();
|
|
@@ -123,6 +158,7 @@ onMounted(async () => {
|
|
|
123
158
|
|
|
124
159
|
async function getSession() {
|
|
125
160
|
if (iap.stripe.client) {
|
|
161
|
+
messageText.value = "";
|
|
126
162
|
try {
|
|
127
163
|
// Fetch client secret from backend
|
|
128
164
|
const clientSecretResponse = await api({
|
|
@@ -131,8 +167,9 @@ async function getSession() {
|
|
|
131
167
|
json: {
|
|
132
168
|
items: props.cart.items,
|
|
133
169
|
sub: props.cart.items.some(
|
|
134
|
-
(i) => i.product.type === ProductType.PAID_SUBSCRIPTION
|
|
170
|
+
(i) => i.product.type === ProductType.PAID_SUBSCRIPTION,
|
|
135
171
|
),
|
|
172
|
+
coupon_code: iap.stripe.coupon,
|
|
136
173
|
},
|
|
137
174
|
});
|
|
138
175
|
if (clientSecretResponse.ok) {
|
|
@@ -142,24 +179,39 @@ async function getSession() {
|
|
|
142
179
|
clientSecret: sessionResponse.value.client_secret,
|
|
143
180
|
elementsOptions: { appearance: { theme: "stripe" } },
|
|
144
181
|
});
|
|
182
|
+
if (checkout && checkout !== null) {
|
|
183
|
+
checkout.on("change", (session: any) => {
|
|
184
|
+
// Handle changes to the checkout session
|
|
185
|
+
console.log("session changed:", session);
|
|
186
|
+
});
|
|
187
|
+
checkout.on("error", (error: any) => {
|
|
188
|
+
console.error("Error initializing checkout:", error);
|
|
189
|
+
});
|
|
190
|
+
checkout.on("ready", (...args: any) => {
|
|
191
|
+
console.info("ready?:", args);
|
|
192
|
+
});
|
|
145
193
|
|
|
146
|
-
|
|
147
|
-
// Handle changes to the checkout session
|
|
148
|
-
console.log("session changed:", session);
|
|
149
|
-
});
|
|
194
|
+
console.log("session created:", sessionResponse.value, checkout);
|
|
150
195
|
|
|
151
|
-
|
|
196
|
+
loadActionsResult = await checkout.loadActions();
|
|
152
197
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
198
|
+
if (
|
|
199
|
+
loadActionsResult.type === "error" &&
|
|
200
|
+
/^No such payment_page: /.test(loadActionsResult.error.message)
|
|
201
|
+
) {
|
|
202
|
+
} else if (loadActionsResult.type !== "success") {
|
|
203
|
+
showPaymentMessage("Failed to load payment actions");
|
|
204
|
+
}
|
|
156
205
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
206
|
+
if (paymentElement) {
|
|
207
|
+
await paymentElement.destroy();
|
|
208
|
+
await nextTick();
|
|
209
|
+
}
|
|
210
|
+
paymentElement = checkout.createPaymentElement();
|
|
211
|
+
paymentElement.mount("#payment-element");
|
|
212
|
+
} else {
|
|
213
|
+
console.error("failed to create checkout");
|
|
160
214
|
}
|
|
161
|
-
paymentElement = checkout.createPaymentElement();
|
|
162
|
-
paymentElement.mount("#payment-element");
|
|
163
215
|
} else {
|
|
164
216
|
const errorContent = clientSecretResponse.body.error;
|
|
165
217
|
iap.stripe.error = new Error(errorContent);
|
|
@@ -173,6 +225,34 @@ async function getSession() {
|
|
|
173
225
|
}
|
|
174
226
|
}
|
|
175
227
|
|
|
228
|
+
async function applyCoupon() {
|
|
229
|
+
if (!couponInput.value.trim()) return;
|
|
230
|
+
couponLoading.value = true;
|
|
231
|
+
couponError.value = "";
|
|
232
|
+
try {
|
|
233
|
+
const response = await api({
|
|
234
|
+
url: `purchases/coupon/?code=${encodeURIComponent(couponInput.value.trim())}`,
|
|
235
|
+
method: "GET",
|
|
236
|
+
});
|
|
237
|
+
if (response.ok) {
|
|
238
|
+
appliedCoupon.value = response.body;
|
|
239
|
+
iap.stripe.coupon = couponInput.value.trim();
|
|
240
|
+
couponInput.value = "";
|
|
241
|
+
} else {
|
|
242
|
+
couponError.value = response.body.error || "Invalid coupon";
|
|
243
|
+
}
|
|
244
|
+
} catch (e) {
|
|
245
|
+
console.log("e", e);
|
|
246
|
+
couponError.value = "Failed to validate coupon";
|
|
247
|
+
}
|
|
248
|
+
couponLoading.value = false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function removeCoupon() {
|
|
252
|
+
appliedCoupon.value = null;
|
|
253
|
+
iap.stripe.coupon = "";
|
|
254
|
+
}
|
|
255
|
+
|
|
176
256
|
function showPaymentMessage(messageContent: string) {
|
|
177
257
|
messageText.value = messageContent;
|
|
178
258
|
|
|
@@ -186,7 +266,6 @@ function setLoading(loading: boolean) {
|
|
|
186
266
|
}
|
|
187
267
|
|
|
188
268
|
async function handleSubmit() {
|
|
189
|
-
console.log("Submitting payment");
|
|
190
269
|
setLoading(true);
|
|
191
270
|
emits("start");
|
|
192
271
|
|
|
@@ -210,7 +289,6 @@ async function handleSubmit() {
|
|
|
210
289
|
}
|
|
211
290
|
|
|
212
291
|
setLoading(false);
|
|
213
|
-
console.log("done Submitting payment");
|
|
214
292
|
}
|
|
215
293
|
</script>
|
|
216
294
|
|
|
@@ -223,4 +301,7 @@ async function handleSubmit() {
|
|
|
223
301
|
.cart {
|
|
224
302
|
min-width: 250px;
|
|
225
303
|
}
|
|
304
|
+
.coupon-applied {
|
|
305
|
+
color: green;
|
|
306
|
+
}
|
|
226
307
|
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@virgodev/iap",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"main": "./index.ts",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
"license": "ISC",
|
|
10
10
|
"description": "",
|
|
11
11
|
"dependencies": {
|
|
12
|
+
"@capacitor/core": "^7.4.4",
|
|
12
13
|
"@stripe/stripe-js": "^8.2.0",
|
|
13
14
|
"@virgodev/iap": "^1.0.1",
|
|
14
|
-
"cordova-plugin-purchase": "^13.12.1"
|
|
15
|
+
"cordova-plugin-purchase": "^13.12.1",
|
|
16
|
+
"localforage": "^1.10.0"
|
|
15
17
|
},
|
|
16
18
|
"peerDependencies": {
|
|
17
19
|
"@capacitor/app": "^7.1.0",
|
package/stores/iap.ts
CHANGED
|
@@ -129,6 +129,9 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
129
129
|
const stripeCallback =
|
|
130
130
|
ref<(t: Transaction | undefined) => Transaction | undefined>();
|
|
131
131
|
const stripeError = ref<undefined | string>();
|
|
132
|
+
const stripeCoupon = ref("");
|
|
133
|
+
|
|
134
|
+
const mobileCoupon = ref("");
|
|
132
135
|
|
|
133
136
|
const storePlatform = computed(() => {
|
|
134
137
|
if (platform.value === "android") {
|
|
@@ -199,8 +202,9 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
199
202
|
const { store } = window.CdvPurchase;
|
|
200
203
|
const product = store.get(code, storePlatform.value);
|
|
201
204
|
if (product) {
|
|
202
|
-
const
|
|
203
|
-
|
|
205
|
+
const coupon = mobileCoupon.value || undefined;
|
|
206
|
+
const offer = product.getOffer(coupon) ?? product.getOffer();
|
|
207
|
+
return offer.order(coupon ? { applicationUsername: coupon } : undefined);
|
|
204
208
|
} else {
|
|
205
209
|
console.error("Product not found");
|
|
206
210
|
}
|
|
@@ -258,6 +262,7 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
258
262
|
transaction: p,
|
|
259
263
|
platform: p.platform,
|
|
260
264
|
subscription: sub,
|
|
265
|
+
coupon_code: mobileCoupon.value || undefined,
|
|
261
266
|
},
|
|
262
267
|
});
|
|
263
268
|
|
|
@@ -267,6 +272,21 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
267
272
|
}
|
|
268
273
|
}
|
|
269
274
|
|
|
275
|
+
async function redeemAppleCode(): Promise<void> {
|
|
276
|
+
if (platform.value !== "ios") {
|
|
277
|
+
throw new Error("Offer code redemption is only supported on iOS");
|
|
278
|
+
}
|
|
279
|
+
if (!window.CdvPurchase) {
|
|
280
|
+
throw new Error("CdvPurchase not available");
|
|
281
|
+
}
|
|
282
|
+
const { store } = window.CdvPurchase;
|
|
283
|
+
if (typeof store.presentCodeRedemptionSheet === "function") {
|
|
284
|
+
await store.presentCodeRedemptionSheet();
|
|
285
|
+
} else {
|
|
286
|
+
throw new Error("presentCodeRedemptionSheet is not supported in this version of CdvPurchase");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
270
290
|
function getCdvProducts(): any[] {
|
|
271
291
|
const { store } = window.CdvPurchase;
|
|
272
292
|
return store.products;
|
|
@@ -309,6 +329,11 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
309
329
|
purchases,
|
|
310
330
|
ready,
|
|
311
331
|
|
|
332
|
+
mobile: {
|
|
333
|
+
coupon: mobileCoupon,
|
|
334
|
+
redeemAppleCode,
|
|
335
|
+
},
|
|
336
|
+
|
|
312
337
|
stripe: {
|
|
313
338
|
key: STRIP_KEY,
|
|
314
339
|
client: stripe,
|
|
@@ -318,6 +343,7 @@ export const useIapStore = defineStore("iap", () => {
|
|
|
318
343
|
result: stripeResult,
|
|
319
344
|
callback: stripeCallback,
|
|
320
345
|
error: stripeError,
|
|
346
|
+
coupon: stripeCoupon,
|
|
321
347
|
reset,
|
|
322
348
|
},
|
|
323
349
|
};
|