@capivv/capacitor-vue 0.1.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/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@capivv/capacitor-vue",
3
+ "version": "0.1.1",
4
+ "description": "Vue components for Capivv Capacitor SDK — paywall",
5
+ "main": "src/index.ts",
6
+ "module": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "files": [
9
+ "src/"
10
+ ],
11
+ "author": "Capivv",
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/capivv/capivv",
16
+ "directory": "sdks/capacitor-vue"
17
+ },
18
+ "keywords": [
19
+ "capacitor",
20
+ "ionic",
21
+ "vue",
22
+ "paywall",
23
+ "subscriptions",
24
+ "in-app-purchases",
25
+ "capivv"
26
+ ],
27
+ "peerDependencies": {
28
+ "@capivv/capacitor-sdk": ">=0.1.0",
29
+ "@capacitor/core": ">=6.0.0",
30
+ "@ionic/vue": ">=7.0.0",
31
+ "ionicons": ">=7.0.0",
32
+ "vue": ">=3.3.0"
33
+ }
34
+ }
@@ -0,0 +1,469 @@
1
+ <template>
2
+ <ion-content :class="['capivv-paywall', { dark: darkMode }]">
3
+ <div class="paywall-container">
4
+ <!-- Header -->
5
+ <div class="paywall-header">
6
+ <ion-button v-if="showCloseButton" fill="clear" @click="$emit('dismiss')">
7
+ <ion-icon :icon="closeOutline"></ion-icon>
8
+ </ion-button>
9
+ <h1 class="paywall-title">{{ title }}</h1>
10
+ <p v-if="subtitle" class="paywall-subtitle">{{ subtitle }}</p>
11
+ </div>
12
+
13
+ <!-- Loading State -->
14
+ <div v-if="loading" class="paywall-loading">
15
+ <ion-spinner></ion-spinner>
16
+ <p>Loading products...</p>
17
+ </div>
18
+
19
+ <!-- Error State -->
20
+ <div v-if="error" class="paywall-error">
21
+ <ion-icon :icon="alertCircleOutline" color="danger"></ion-icon>
22
+ <p>{{ error }}</p>
23
+ <ion-button @click="loadProducts">Retry</ion-button>
24
+ </div>
25
+
26
+ <!-- Products -->
27
+ <div v-if="!loading && !error" class="paywall-products">
28
+ <ion-card
29
+ v-for="product in products"
30
+ :key="product.identifier"
31
+ :class="{ selected: selectedProduct?.identifier === product.identifier }"
32
+ button
33
+ @click="selectProduct(product)"
34
+ >
35
+ <ion-card-header>
36
+ <ion-card-title>{{ product.title }}</ion-card-title>
37
+ <ion-card-subtitle v-if="product.subscriptionPeriod">
38
+ {{ product.subscriptionPeriod }}
39
+ </ion-card-subtitle>
40
+ </ion-card-header>
41
+ <ion-card-content>
42
+ <p class="product-description">{{ product.description }}</p>
43
+ <div class="product-price">
44
+ <span class="price">{{ product.priceString }}</span>
45
+ <span v-if="product.trialPeriod" class="trial-badge">
46
+ {{ product.trialPeriod }} free trial
47
+ </span>
48
+ </div>
49
+ </ion-card-content>
50
+ <ion-icon
51
+ v-if="selectedProduct?.identifier === product.identifier"
52
+ :icon="checkmarkCircle"
53
+ color="primary"
54
+ class="selected-icon"
55
+ ></ion-icon>
56
+ </ion-card>
57
+ </div>
58
+
59
+ <!-- Features List -->
60
+ <div v-if="features.length > 0" class="paywall-features">
61
+ <h3>What you'll get:</h3>
62
+ <ion-list lines="none">
63
+ <ion-item v-for="(feature, index) in features" :key="index">
64
+ <ion-icon :icon="checkmarkCircle" color="success" slot="start"></ion-icon>
65
+ <ion-label>{{ feature }}</ion-label>
66
+ </ion-item>
67
+ </ion-list>
68
+ </div>
69
+
70
+ <!-- Purchase Button -->
71
+ <div class="paywall-actions">
72
+ <ion-button
73
+ expand="block"
74
+ size="large"
75
+ :disabled="!selectedProduct || purchasing"
76
+ @click="purchase"
77
+ >
78
+ <ion-spinner v-if="purchasing"></ion-spinner>
79
+ <span v-else>{{ purchaseButtonText || 'Subscribe Now' }}</span>
80
+ </ion-button>
81
+
82
+ <ion-button
83
+ v-if="showRestoreButton"
84
+ expand="block"
85
+ fill="clear"
86
+ :disabled="restoring"
87
+ @click="restore"
88
+ >
89
+ <ion-spinner v-if="restoring" name="dots"></ion-spinner>
90
+ <span v-else>Restore Purchases</span>
91
+ </ion-button>
92
+ </div>
93
+
94
+ <!-- Footer -->
95
+ <div class="paywall-footer">
96
+ <p class="terms-text">
97
+ <a v-if="termsUrl" @click="openTerms">Terms of Service</a>
98
+ <span v-if="termsUrl && privacyUrl"> • </span>
99
+ <a v-if="privacyUrl" @click="openPrivacy">Privacy Policy</a>
100
+ </p>
101
+ <p v-if="selectedProduct?.productType === 'SUBSCRIPTION'" class="subscription-terms">
102
+ Subscription automatically renews unless cancelled at least 24 hours before the end of the
103
+ current period.
104
+ </p>
105
+ </div>
106
+ </div>
107
+ </ion-content>
108
+ </template>
109
+
110
+ <script lang="ts">
111
+ import { defineComponent, ref, onMounted, onUnmounted, PropType } from 'vue';
112
+ import {
113
+ IonContent,
114
+ IonButton,
115
+ IonIcon,
116
+ IonSpinner,
117
+ IonCard,
118
+ IonCardHeader,
119
+ IonCardTitle,
120
+ IonCardSubtitle,
121
+ IonCardContent,
122
+ IonList,
123
+ IonItem,
124
+ IonLabel,
125
+ } from '@ionic/vue';
126
+ import { closeOutline, checkmarkCircle, alertCircleOutline } from 'ionicons/icons';
127
+ import { Capivv, Product, Offering, PurchaseResult, ProductType } from '@capivv/capacitor-sdk';
128
+ import type { PluginListenerHandle } from '@capacitor/core';
129
+
130
+ /**
131
+ * Capivv Paywall Component for Ionic Vue
132
+ *
133
+ * A ready-to-use paywall component that displays available products
134
+ * and handles the purchase flow.
135
+ *
136
+ * @example
137
+ * ```vue
138
+ * <CapivvPaywall
139
+ * offering-id="default"
140
+ * :show-restore-button="true"
141
+ * @purchase-complete="onPurchaseComplete"
142
+ * @dismiss="closePaywall"
143
+ * />
144
+ * ```
145
+ */
146
+ export default defineComponent({
147
+ name: 'CapivvPaywall',
148
+ components: {
149
+ IonContent,
150
+ IonButton,
151
+ IonIcon,
152
+ IonSpinner,
153
+ IonCard,
154
+ IonCardHeader,
155
+ IonCardTitle,
156
+ IonCardSubtitle,
157
+ IonCardContent,
158
+ IonList,
159
+ IonItem,
160
+ IonLabel,
161
+ },
162
+ props: {
163
+ /** The offering ID to display products from */
164
+ offeringId: {
165
+ type: String,
166
+ default: 'default',
167
+ },
168
+ /** Title displayed at the top of the paywall */
169
+ title: {
170
+ type: String,
171
+ default: 'Unlock Premium',
172
+ },
173
+ /** Subtitle displayed below the title */
174
+ subtitle: {
175
+ type: String,
176
+ default: '',
177
+ },
178
+ /** Whether to show the close button */
179
+ showCloseButton: {
180
+ type: Boolean,
181
+ default: true,
182
+ },
183
+ /** Whether to show the restore purchases button */
184
+ showRestoreButton: {
185
+ type: Boolean,
186
+ default: true,
187
+ },
188
+ /** List of features to display */
189
+ features: {
190
+ type: Array as PropType<string[]>,
191
+ default: () => [],
192
+ },
193
+ /** Custom text for the purchase button */
194
+ purchaseButtonText: {
195
+ type: String,
196
+ default: 'Subscribe Now',
197
+ },
198
+ /** URL for terms of service */
199
+ termsUrl: {
200
+ type: String,
201
+ default: '',
202
+ },
203
+ /** URL for privacy policy */
204
+ privacyUrl: {
205
+ type: String,
206
+ default: '',
207
+ },
208
+ /** Enable dark mode styling */
209
+ darkMode: {
210
+ type: Boolean,
211
+ default: false,
212
+ },
213
+ },
214
+ emits: ['purchase-complete', 'purchase-failed', 'restore-complete', 'dismiss'],
215
+ setup(props, { emit }) {
216
+ const products = ref<Product[]>([]);
217
+ const selectedProduct = ref<Product | null>(null);
218
+ const loading = ref(true);
219
+ const error = ref<string | null>(null);
220
+ const purchasing = ref(false);
221
+ const restoring = ref(false);
222
+ const listeners = ref<PluginListenerHandle[]>([]);
223
+
224
+ const loadProducts = async () => {
225
+ loading.value = true;
226
+ error.value = null;
227
+
228
+ try {
229
+ const { offerings } = await Capivv.getOfferings();
230
+ const offering =
231
+ offerings.find((o: Offering) => o.identifier === props.offeringId) || offerings[0];
232
+
233
+ if (offering) {
234
+ products.value = offering.products;
235
+ if (offering.products.length > 0) {
236
+ selectedProduct.value = offering.products[0];
237
+ }
238
+ }
239
+ } catch (e) {
240
+ error.value = e instanceof Error ? e.message : 'Failed to load products';
241
+ } finally {
242
+ loading.value = false;
243
+ }
244
+ };
245
+
246
+ const setupListeners = async () => {
247
+ const purchaseListener = await Capivv.addListener('purchaseCompleted', (event) => {
248
+ purchasing.value = false;
249
+ emit('purchase-complete', { success: true, transaction: event.transaction });
250
+ });
251
+ listeners.value.push(purchaseListener);
252
+
253
+ const failedListener = await Capivv.addListener('purchaseFailed', (event) => {
254
+ purchasing.value = false;
255
+ emit('purchase-failed', { error: event.error });
256
+ });
257
+ listeners.value.push(failedListener);
258
+ };
259
+
260
+ const selectProduct = (product: Product) => {
261
+ selectedProduct.value = product;
262
+ };
263
+
264
+ const purchase = async () => {
265
+ if (!selectedProduct.value) return;
266
+
267
+ purchasing.value = true;
268
+
269
+ try {
270
+ const result = await Capivv.purchase({
271
+ productIdentifier: selectedProduct.value.identifier,
272
+ productType: selectedProduct.value.productType as ProductType,
273
+ });
274
+
275
+ if (result.success) {
276
+ emit('purchase-complete', result);
277
+ } else {
278
+ emit('purchase-failed', { error: result.error || 'Purchase failed' });
279
+ }
280
+ } catch (e) {
281
+ emit('purchase-failed', { error: e instanceof Error ? e.message : 'Purchase failed' });
282
+ } finally {
283
+ purchasing.value = false;
284
+ }
285
+ };
286
+
287
+ const restore = async () => {
288
+ restoring.value = true;
289
+
290
+ try {
291
+ await Capivv.restorePurchases();
292
+ emit('restore-complete');
293
+ } catch (e) {
294
+ emit('purchase-failed', { error: e instanceof Error ? e.message : 'Restore failed' });
295
+ } finally {
296
+ restoring.value = false;
297
+ }
298
+ };
299
+
300
+ const openTerms = () => {
301
+ if (props.termsUrl) {
302
+ window.open(props.termsUrl, '_blank');
303
+ }
304
+ };
305
+
306
+ const openPrivacy = () => {
307
+ if (props.privacyUrl) {
308
+ window.open(props.privacyUrl, '_blank');
309
+ }
310
+ };
311
+
312
+ onMounted(async () => {
313
+ await loadProducts();
314
+ await setupListeners();
315
+ });
316
+
317
+ onUnmounted(() => {
318
+ listeners.value.forEach((listener) => listener.remove());
319
+ });
320
+
321
+ return {
322
+ products,
323
+ selectedProduct,
324
+ loading,
325
+ error,
326
+ purchasing,
327
+ restoring,
328
+ loadProducts,
329
+ selectProduct,
330
+ purchase,
331
+ restore,
332
+ openTerms,
333
+ openPrivacy,
334
+ closeOutline,
335
+ checkmarkCircle,
336
+ alertCircleOutline,
337
+ };
338
+ },
339
+ });
340
+ </script>
341
+
342
+ <style scoped>
343
+ .capivv-paywall {
344
+ --background: var(--ion-background-color, #ffffff);
345
+ }
346
+
347
+ .capivv-paywall.dark {
348
+ --background: var(--ion-background-color, #1a1a1a);
349
+ }
350
+
351
+ .paywall-container {
352
+ padding: 16px;
353
+ max-width: 500px;
354
+ margin: 0 auto;
355
+ }
356
+
357
+ .paywall-header {
358
+ text-align: center;
359
+ margin-bottom: 24px;
360
+ }
361
+
362
+ .paywall-title {
363
+ font-size: 24px;
364
+ font-weight: 700;
365
+ margin: 16px 0 8px;
366
+ }
367
+
368
+ .paywall-subtitle {
369
+ font-size: 16px;
370
+ color: var(--ion-color-medium);
371
+ margin: 0;
372
+ }
373
+
374
+ .paywall-loading,
375
+ .paywall-error {
376
+ text-align: center;
377
+ padding: 48px 16px;
378
+ }
379
+
380
+ .paywall-error ion-icon {
381
+ font-size: 48px;
382
+ margin-bottom: 16px;
383
+ }
384
+
385
+ .paywall-products {
386
+ margin-bottom: 24px;
387
+ }
388
+
389
+ ion-card {
390
+ margin: 12px 0;
391
+ position: relative;
392
+ border: 2px solid transparent;
393
+ transition: border-color 0.2s ease;
394
+ }
395
+
396
+ ion-card.selected {
397
+ border-color: var(--ion-color-primary);
398
+ }
399
+
400
+ .selected-icon {
401
+ position: absolute;
402
+ top: 12px;
403
+ right: 12px;
404
+ font-size: 24px;
405
+ }
406
+
407
+ .product-description {
408
+ color: var(--ion-color-medium);
409
+ margin-bottom: 12px;
410
+ }
411
+
412
+ .product-price {
413
+ display: flex;
414
+ align-items: center;
415
+ gap: 8px;
416
+ }
417
+
418
+ .price {
419
+ font-size: 20px;
420
+ font-weight: 700;
421
+ color: var(--ion-color-primary);
422
+ }
423
+
424
+ .trial-badge {
425
+ background: var(--ion-color-success);
426
+ color: white;
427
+ padding: 4px 8px;
428
+ border-radius: 4px;
429
+ font-size: 12px;
430
+ font-weight: 600;
431
+ }
432
+
433
+ .paywall-features {
434
+ margin-bottom: 24px;
435
+ }
436
+
437
+ .paywall-features h3 {
438
+ margin-bottom: 12px;
439
+ font-weight: 600;
440
+ }
441
+
442
+ .paywall-actions {
443
+ margin-bottom: 24px;
444
+ }
445
+
446
+ .paywall-actions ion-button {
447
+ margin-bottom: 8px;
448
+ }
449
+
450
+ .paywall-footer {
451
+ text-align: center;
452
+ }
453
+
454
+ .terms-text {
455
+ font-size: 14px;
456
+ color: var(--ion-color-medium);
457
+ }
458
+
459
+ .terms-text a {
460
+ color: var(--ion-color-primary);
461
+ cursor: pointer;
462
+ }
463
+
464
+ .subscription-terms {
465
+ font-size: 12px;
466
+ color: var(--ion-color-medium);
467
+ margin-top: 8px;
468
+ }
469
+ </style>
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default as CapivvPaywall } from './CapivvPaywall.vue';