@payvia-sdk/sdk 1.1.4
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/README.md +463 -0
- package/package.json +32 -0
- package/payvia.js +696 -0
package/README.md
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# PayVia SDK
|
|
2
|
+
|
|
3
|
+
A lightweight JavaScript SDK for connecting your Chrome Extension / SaaS app to PayVia, for accepting PayPal payments and managing subscriptions, license validation, tier-based feature gating, trial management and monthly / yearly / lifetime plans.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
The `sample-extension` comes pre-configured with a shared demo project and works out of the box:
|
|
8
|
+
|
|
9
|
+
1. Open `chrome://extensions/`
|
|
10
|
+
2. Enable "Developer mode"
|
|
11
|
+
3. Click "Load unpacked" and select the `sample-extension` folder
|
|
12
|
+
4. Click the extension icon — everything works!
|
|
13
|
+
|
|
14
|
+
> **Note:** The demo project is shared across all users and is meant for testing only.
|
|
15
|
+
> To accept real payments, create your own project at [PayVia Dashboard](https://payvia.site/dashboard).
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Option 1: Copy directly
|
|
20
|
+
|
|
21
|
+
Copy `payvia.js` into your extension folder.
|
|
22
|
+
|
|
23
|
+
### Option 2: npm
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install payvia-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Basic Usage
|
|
30
|
+
|
|
31
|
+
### 1. Add to manifest.json
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"manifest_version": 3,
|
|
36
|
+
"name": "Your Extension",
|
|
37
|
+
"permissions": ["storage"],
|
|
38
|
+
"host_permissions": [
|
|
39
|
+
"https://api.payvia.site/*",
|
|
40
|
+
"https://payvia.site/*"
|
|
41
|
+
],
|
|
42
|
+
"background": {
|
|
43
|
+
"service_worker": "background.js"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If using Google Identity for automatic user detection, also add:
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"permissions": ["storage", "identity", "identity.email"]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Initialize in background.js
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
import PayVia from './payvia.js';
|
|
59
|
+
|
|
60
|
+
const payvia = PayVia('YOUR_API_KEY');
|
|
61
|
+
|
|
62
|
+
// Check if user has paid
|
|
63
|
+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
64
|
+
if (request.action === 'checkPaid') {
|
|
65
|
+
payvia.getUser().then(user => {
|
|
66
|
+
sendResponse({ paid: user.paid });
|
|
67
|
+
});
|
|
68
|
+
return true; // async response
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 3. Check payment status
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
const user = await payvia.getUser();
|
|
77
|
+
|
|
78
|
+
if (user.paid) {
|
|
79
|
+
enablePremiumFeatures();
|
|
80
|
+
} else {
|
|
81
|
+
showUpgradeButton();
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 4. Open payment page
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
// Option 1: Pricing page — shows all plans (recommended)
|
|
89
|
+
await payvia.openPaymentPage({ mode: 'pricing', email: userEmail });
|
|
90
|
+
|
|
91
|
+
// Option 2: Hosted checkout — specific plan
|
|
92
|
+
await payvia.openPaymentPage({ mode: 'hosted', planId: 'your-plan-id', email: userEmail });
|
|
93
|
+
|
|
94
|
+
// Option 3: Direct PayPal — no PayVia UI
|
|
95
|
+
await payvia.openPaymentPage({ mode: 'direct', planId: 'your-plan-id', email: userEmail });
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 5. Listen for payment changes
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
const unsubscribe = payvia.onPaid((user) => {
|
|
102
|
+
console.log('User just paid!', user);
|
|
103
|
+
enablePremiumFeatures();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Call unsubscribe() to stop listening
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Checkout Modes
|
|
112
|
+
|
|
113
|
+
| Mode | Description | When to use |
|
|
114
|
+
|------|-------------|-------------|
|
|
115
|
+
| `pricing` | PayVia plan selection page | **Default** — when you have multiple plans |
|
|
116
|
+
| `hosted` | PayVia checkout for a specific plan | When you have one plan or want to skip selection |
|
|
117
|
+
| `direct` | Straight to PayPal, no PayVia UI | For developers who want full UI control |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## User Identity
|
|
122
|
+
|
|
123
|
+
The SDK automatically identifies users:
|
|
124
|
+
|
|
125
|
+
1. **Google Identity** — if the extension has the `identity` permission, uses the user's Google email
|
|
126
|
+
2. **Random ID fallback** — generates a persistent `pv_`-prefixed UUID, synced via `chrome.storage.sync`
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
const identity = await payvia.getIdentity();
|
|
130
|
+
// { id: "user@gmail.com", email: "user@gmail.com", source: "google" }
|
|
131
|
+
// or: { id: "pv_abc123...", email: null, source: "random" }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Tier-Based Feature Gating
|
|
137
|
+
|
|
138
|
+
Tiers define feature sets (Free/Pro/Super). Plans are pricing options within tiers.
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
// Check tier level (0=Free, 1=Pro, 2=Super)
|
|
142
|
+
const hasPro = await payvia.hasTierLevel(1); // Pro or above
|
|
143
|
+
|
|
144
|
+
// Check specific feature
|
|
145
|
+
const canExport = await payvia.hasFeature('export_pdf');
|
|
146
|
+
|
|
147
|
+
// Get full tier info
|
|
148
|
+
const tier = await payvia.getTier();
|
|
149
|
+
// { id: "...", name: "Pro", level: 1, features: ["export_pdf", "api_access"] }
|
|
150
|
+
|
|
151
|
+
// Or via user object
|
|
152
|
+
const user = await payvia.getUser();
|
|
153
|
+
// user.tier — tier object
|
|
154
|
+
// user.features — shortcut to user.tier.features
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Trial Support
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
// Start trial on first use (idempotent — safe to call multiple times)
|
|
163
|
+
if (await payvia.isFirstRun()) {
|
|
164
|
+
const trial = await payvia.startTrial();
|
|
165
|
+
if (trial) {
|
|
166
|
+
console.log(`Trial started! ${trial.daysRemaining} days remaining`);
|
|
167
|
+
// { subscriptionId, status, planId, planName, trialExpiresAt, daysRemaining }
|
|
168
|
+
}
|
|
169
|
+
await payvia.markFirstRunDone();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check trial status
|
|
173
|
+
const status = await payvia.getTrialStatus();
|
|
174
|
+
// { status, trialExpiresAt, daysRemaining, canConvert, planIds }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Trial info is also available on the user object:
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
const user = await payvia.getUser();
|
|
181
|
+
// user.isTrial — boolean
|
|
182
|
+
// user.trialExpiresAt — Date or null
|
|
183
|
+
// user.daysRemaining — number or null
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## License Caching
|
|
189
|
+
|
|
190
|
+
The SDK caches license data for offline resilience. No setup required — it works automatically.
|
|
191
|
+
|
|
192
|
+
- **TTL**: 7 days (server-controlled)
|
|
193
|
+
- **Grace period**: 30 days (allows offline access after TTL expires)
|
|
194
|
+
- **Storage**: `chrome.storage.local` (extension) or `localStorage` (web)
|
|
195
|
+
- **Anti-tamper**: HMAC-SHA256 signature verification
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
// Refresh cache in background (e.g., service worker startup)
|
|
199
|
+
chrome.runtime.onStartup.addListener(async () => {
|
|
200
|
+
await payvia.refreshLicenseCache();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Check if data came from cache
|
|
204
|
+
const user = await payvia.getUser();
|
|
205
|
+
if (user.fromCache) {
|
|
206
|
+
// Using cached data — works offline
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## API Reference
|
|
213
|
+
|
|
214
|
+
### `PayVia(apiKey)`
|
|
215
|
+
|
|
216
|
+
Creates a new PayVia instance.
|
|
217
|
+
|
|
218
|
+
| Parameter | Type | Description |
|
|
219
|
+
|-----------|------|-------------|
|
|
220
|
+
| `apiKey` | string | API key from the PayVia dashboard |
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
### `payvia.getUser(options?)`
|
|
225
|
+
|
|
226
|
+
Returns the user's payment status and subscription info. Uses cache automatically.
|
|
227
|
+
|
|
228
|
+
**Options:** `{ forceRefresh: boolean }`
|
|
229
|
+
|
|
230
|
+
**Returns:** `Promise<PayViaUser>`
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
interface PayViaUser {
|
|
234
|
+
id: string; // Unique user identifier
|
|
235
|
+
email: string | null; // User email (if available)
|
|
236
|
+
identitySource: 'google' | 'random'; // Identity source
|
|
237
|
+
paid: boolean; // true if ACTIVE or TRIAL
|
|
238
|
+
status: 'ACTIVE' | 'TRIAL' | 'INACTIVE'; // Subscription status
|
|
239
|
+
tier: { // Current tier (or null)
|
|
240
|
+
id: string;
|
|
241
|
+
name: string;
|
|
242
|
+
level: number; // 0=Free, 1=Pro, 2=Super
|
|
243
|
+
features: string[];
|
|
244
|
+
} | null;
|
|
245
|
+
features: string[]; // Shortcut to tier.features
|
|
246
|
+
planIds: string[]; // Purchased plan IDs
|
|
247
|
+
isTrial: boolean; // Whether on trial
|
|
248
|
+
trialExpiresAt: Date | null; // Trial expiration date
|
|
249
|
+
daysRemaining: number | null; // Trial days remaining
|
|
250
|
+
fromCache: boolean; // true if data came from cache
|
|
251
|
+
checkedAt: number | null; // Unix timestamp (ms)
|
|
252
|
+
ttl: number | null; // Cache TTL in ms
|
|
253
|
+
signature: string | null; // HMAC anti-tamper signature
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
### `payvia.refresh()`
|
|
260
|
+
|
|
261
|
+
Force-refreshes user status from the server (bypasses cache).
|
|
262
|
+
|
|
263
|
+
**Returns:** `Promise<PayViaUser>`
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### `payvia.refreshLicenseCache()`
|
|
268
|
+
|
|
269
|
+
Refreshes the license cache if expired. Ideal for background service worker startup.
|
|
270
|
+
|
|
271
|
+
**Returns:** `Promise<void>`
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
### `payvia.openPaymentPage(options)`
|
|
276
|
+
|
|
277
|
+
Opens the payment page in a new tab.
|
|
278
|
+
|
|
279
|
+
| Option | Type | Required | Description |
|
|
280
|
+
|--------|------|----------|-------------|
|
|
281
|
+
| `mode` | `'pricing' \| 'hosted' \| 'direct'` | No | Checkout mode (default: `pricing`) |
|
|
282
|
+
| `planId` | string | For hosted/direct | Plan ID to purchase |
|
|
283
|
+
| `email` | string | No | Customer email |
|
|
284
|
+
| `successUrl` | string | For direct | Redirect URL after successful payment |
|
|
285
|
+
| `cancelUrl` | string | For direct | Redirect URL if user cancels |
|
|
286
|
+
|
|
287
|
+
**Returns:** `Promise<{ mode, pricingUrl? | checkoutUrl? }>`
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
### `payvia.onPaid(callback)`
|
|
292
|
+
|
|
293
|
+
Listens for payment status changes (polls every 5 seconds).
|
|
294
|
+
|
|
295
|
+
**Returns:** `Function` — call it to stop listening
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
### `payvia.hasFeature(name)`
|
|
300
|
+
|
|
301
|
+
Checks if the user's tier includes a specific feature.
|
|
302
|
+
|
|
303
|
+
**Returns:** `Promise<boolean>`
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
### `payvia.hasTierLevel(level)`
|
|
308
|
+
|
|
309
|
+
Checks if the user's tier is at or above the required level.
|
|
310
|
+
|
|
311
|
+
**Returns:** `Promise<boolean>`
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
### `payvia.getTier()`
|
|
316
|
+
|
|
317
|
+
Returns the user's current tier info.
|
|
318
|
+
|
|
319
|
+
**Returns:** `Promise<{ id, name, level, features } | null>`
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
### `payvia.getIdentity()`
|
|
324
|
+
|
|
325
|
+
Returns the current user identity.
|
|
326
|
+
|
|
327
|
+
**Returns:** `Promise<{ id: string, email: string | null, source: 'google' | 'random' }>`
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
### `payvia.needsEmailForPayment()`
|
|
332
|
+
|
|
333
|
+
Checks if an email prompt is needed (user has no Google identity).
|
|
334
|
+
|
|
335
|
+
**Returns:** `Promise<boolean>`
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
### `payvia.getPlans()`
|
|
340
|
+
|
|
341
|
+
Returns available plans for the project.
|
|
342
|
+
|
|
343
|
+
**Returns:** `Promise<Plan[]>`
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
### `payvia.startTrial()`
|
|
348
|
+
|
|
349
|
+
Starts a trial for the current user. Idempotent — safe to call multiple times.
|
|
350
|
+
|
|
351
|
+
**Returns:** `Promise<{ subscriptionId, status, planId, planName, trialExpiresAt, daysRemaining } | null>`
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
### `payvia.getTrialStatus()`
|
|
356
|
+
|
|
357
|
+
Returns the trial status for the current user.
|
|
358
|
+
|
|
359
|
+
**Returns:** `Promise<{ status, trialExpiresAt, daysRemaining, canConvert, planIds }>`
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
### `payvia.isFirstRun()`
|
|
364
|
+
|
|
365
|
+
Checks if the extension is being used for the first time.
|
|
366
|
+
|
|
367
|
+
**Returns:** `Promise<boolean>`
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
### `payvia.markFirstRunDone()`
|
|
372
|
+
|
|
373
|
+
Marks the first run as complete.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
### `payvia.cancelSubscription(options?)`
|
|
378
|
+
|
|
379
|
+
Cancels the user's subscription.
|
|
380
|
+
|
|
381
|
+
| Option | Type | Required | Description |
|
|
382
|
+
|--------|------|----------|-------------|
|
|
383
|
+
| `planId` | string | No | Specific plan to cancel |
|
|
384
|
+
| `reason` | string | No | Cancellation reason |
|
|
385
|
+
|
|
386
|
+
**Returns:** `Promise<{ success, message, canceledPlanId }>`
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
### `payvia.resetLicense()`
|
|
391
|
+
|
|
392
|
+
Resets the user's license (for testing/demo purposes). Deletes all subscriptions for the current user.
|
|
393
|
+
|
|
394
|
+
**Returns:** `Promise<{ message }>`
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Plan Description Format
|
|
399
|
+
|
|
400
|
+
In the PayVia Dashboard, you can write plan descriptions with feature lists:
|
|
401
|
+
|
|
402
|
+
```
|
|
403
|
+
Everything in Free, plus:
|
|
404
|
+
+++Unlimited access
|
|
405
|
+
+++Priority support
|
|
406
|
+
+++Custom features
|
|
407
|
+
Contact us for enterprise plans
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Lines starting with `+++` are displayed with a green checkmark on checkout pages.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Sample Extension
|
|
415
|
+
|
|
416
|
+
See the `sample-extension/` folder for a working example that demonstrates:
|
|
417
|
+
|
|
418
|
+
1. **Payment status check** — automatic user detection
|
|
419
|
+
2. **Three checkout modes** — Pricing Page, Hosted Checkout, Direct PayPal
|
|
420
|
+
3. **Feature-per-Plan** — each plan unlocks different features
|
|
421
|
+
4. **Smart identity** — Google Identity and Random ID support
|
|
422
|
+
5. **Reset demo** — button to reset license for repeated testing
|
|
423
|
+
|
|
424
|
+
### Running the sample:
|
|
425
|
+
|
|
426
|
+
1. Open `chrome://extensions/`
|
|
427
|
+
2. Enable Developer mode
|
|
428
|
+
3. Click "Load unpacked"
|
|
429
|
+
4. Select the `sdk/sample-extension/` folder
|
|
430
|
+
5. Click the extension icon
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## FAQ
|
|
435
|
+
|
|
436
|
+
### How is the user identified?
|
|
437
|
+
|
|
438
|
+
PayVia uses `chrome.storage.sync` to create a unique ID that persists even if the user uninstalls the extension. If the user is signed into Chrome and you have the `identity` permission, identification uses their Google email instead.
|
|
439
|
+
|
|
440
|
+
### What happens offline?
|
|
441
|
+
|
|
442
|
+
The SDK uses cached license data with a 7-day TTL and 30-day grace period. During network outages within the grace period, the cached status is returned. If no valid cache exists, `getUser()` returns `{ paid: false, status: 'INACTIVE' }`.
|
|
443
|
+
|
|
444
|
+
### What's the difference between checkout modes?
|
|
445
|
+
|
|
446
|
+
- **pricing** — user sees all plans and picks one
|
|
447
|
+
- **hosted** — user sees a styled checkout page for a specific plan
|
|
448
|
+
- **direct** — user is sent straight to PayPal with no PayVia UI
|
|
449
|
+
|
|
450
|
+
### How do I test in development?
|
|
451
|
+
|
|
452
|
+
Use PayPal Sandbox credentials and PayPal Sandbox accounts.
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Resources
|
|
457
|
+
|
|
458
|
+
- **Dashboard**: https://payvia.site
|
|
459
|
+
- **API Base URL**: https://api.payvia.site
|
|
460
|
+
- **Sample Extension**: `sample-extension/` in this repo
|
|
461
|
+
- **Sample SaaS App**: `sample-saas/` in this repo
|
|
462
|
+
- **MCP Server** (for AI agents): See [`mcp-server/`](mcp-server/) in this repo
|
|
463
|
+
- **AI Agent Skill**: See [`skill.md`](skill.md) in this repo
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@payvia-sdk/sdk",
|
|
3
|
+
"version": "1.1.4",
|
|
4
|
+
"description": "A lightweight JavaScript SDK for connecting your Chrome Extension / SaaS app to PayVia, for accepting PayPal payments and managing subscriptions, license validation, tier-based feature gating, trial management and monthly / yearly / lifetime plans",
|
|
5
|
+
"main": "payvia.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./payvia.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"payvia.js",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"paypal",
|
|
16
|
+
"payments",
|
|
17
|
+
"chrome-extension",
|
|
18
|
+
"saas",
|
|
19
|
+
"subscriptions",
|
|
20
|
+
"monetization"
|
|
21
|
+
],
|
|
22
|
+
"author": "PayVia",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"homepage": "https://payvia.site",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/Asia-Digital/PayVia-SDK.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/Asia-Digital/PayVia-SDK/issues"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/payvia.js
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PayVia SDK for Chrome Extensions
|
|
3
|
+
*
|
|
4
|
+
* ספריית JavaScript שהסולק מטמיע בתוסף הכרום שלו
|
|
5
|
+
* מאפשרת בדיקת רישיון, פתיחת חלון תשלום, וניהול מנויים
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```javascript
|
|
9
|
+
* import PayVia from './payvia.js';
|
|
10
|
+
*
|
|
11
|
+
* const payvia = PayVia('YOUR_API_KEY');
|
|
12
|
+
*
|
|
13
|
+
* // Check if user paid
|
|
14
|
+
* const user = await payvia.getUser();
|
|
15
|
+
* if (user.paid) {
|
|
16
|
+
* // Enable premium features
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* // Open payment page
|
|
20
|
+
* payvia.openPaymentPage();
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function PayVia(apiKey) {
|
|
25
|
+
if (!apiKey) {
|
|
26
|
+
throw new Error('PayVia: API key is required');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const PAYVIA_API_URL = 'https://api.payvia.site';
|
|
30
|
+
|
|
31
|
+
const LICENSE_CACHE_KEY = 'payvia_license_cache';
|
|
32
|
+
const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
33
|
+
const GRACE_PERIOD_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
34
|
+
|
|
35
|
+
const instance = {};
|
|
36
|
+
let cachedUser = null;
|
|
37
|
+
let userPromise = null;
|
|
38
|
+
|
|
39
|
+
// ============ License Cache Storage ============
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get storage interface (chrome.storage.local or localStorage)
|
|
43
|
+
*/
|
|
44
|
+
function getStorage() {
|
|
45
|
+
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
|
|
46
|
+
return {
|
|
47
|
+
async get(key) {
|
|
48
|
+
return new Promise(resolve => {
|
|
49
|
+
chrome.storage.local.get([key], result => resolve(result[key]));
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
async set(key, value) {
|
|
53
|
+
return new Promise(resolve => {
|
|
54
|
+
chrome.storage.local.set({ [key]: value }, resolve);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Fallback to localStorage
|
|
60
|
+
return {
|
|
61
|
+
async get(key) {
|
|
62
|
+
const value = localStorage.getItem(key);
|
|
63
|
+
return value ? JSON.parse(value) : null;
|
|
64
|
+
},
|
|
65
|
+
async set(key, value) {
|
|
66
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get cached license data
|
|
73
|
+
*/
|
|
74
|
+
async function getCachedLicense() {
|
|
75
|
+
try {
|
|
76
|
+
const storage = getStorage();
|
|
77
|
+
return await storage.get(LICENSE_CACHE_KEY);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.warn('PayVia: Failed to read license cache', error);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Save license data to cache
|
|
86
|
+
*/
|
|
87
|
+
async function setCachedLicense(data) {
|
|
88
|
+
try {
|
|
89
|
+
const storage = getStorage();
|
|
90
|
+
await storage.set(LICENSE_CACHE_KEY, data);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.warn('PayVia: Failed to write license cache', error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if cache is still valid (within TTL)
|
|
98
|
+
*/
|
|
99
|
+
function isCacheValid(cache) {
|
|
100
|
+
if (!cache || !cache.checkedAt) return false;
|
|
101
|
+
const ttl = cache.ttl || DEFAULT_TTL_MS;
|
|
102
|
+
return Date.now() < cache.checkedAt + ttl;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if cache is within grace period (expired but still usable offline)
|
|
107
|
+
*/
|
|
108
|
+
function isWithinGracePeriod(cache) {
|
|
109
|
+
if (!cache || !cache.checkedAt) return false;
|
|
110
|
+
const ttl = cache.ttl || DEFAULT_TTL_MS;
|
|
111
|
+
return Date.now() < cache.checkedAt + ttl + GRACE_PERIOD_MS;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let cachedIdentity = null; // Stores { id, email, source }
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Try to get the user's Google account email via chrome.identity
|
|
118
|
+
* @returns {Promise<{email: string, source: 'google'} | null>}
|
|
119
|
+
*/
|
|
120
|
+
async function getGoogleIdentity() {
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
if (typeof chrome !== 'undefined' && chrome.identity && chrome.identity.getProfileUserInfo) {
|
|
123
|
+
chrome.identity.getProfileUserInfo({ accountStatus: 'ANY' }, (userInfo) => {
|
|
124
|
+
if (chrome.runtime.lastError) {
|
|
125
|
+
console.log('PayVia: Could not get Google identity:', chrome.runtime.lastError.message);
|
|
126
|
+
resolve(null);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (userInfo && userInfo.email) {
|
|
130
|
+
resolve({ email: userInfo.email, source: 'google' });
|
|
131
|
+
} else {
|
|
132
|
+
resolve(null);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
resolve(null);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get or generate a random fallback ID
|
|
143
|
+
* Uses chrome.storage.sync to persist across devices
|
|
144
|
+
* @returns {Promise<string>}
|
|
145
|
+
*/
|
|
146
|
+
async function getRandomId() {
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
|
|
149
|
+
chrome.storage.sync.get(['payvia_user_id'], (result) => {
|
|
150
|
+
if (result.payvia_user_id) {
|
|
151
|
+
resolve(result.payvia_user_id);
|
|
152
|
+
} else {
|
|
153
|
+
const newUserId = 'pv_' + generateUUID();
|
|
154
|
+
chrome.storage.sync.set({ payvia_user_id: newUserId }, () => {
|
|
155
|
+
resolve(newUserId);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
// Fallback for non-extension environments (testing)
|
|
161
|
+
let userId = localStorage.getItem('payvia_user_id');
|
|
162
|
+
if (!userId) {
|
|
163
|
+
userId = 'pv_' + generateUUID();
|
|
164
|
+
localStorage.setItem('payvia_user_id', userId);
|
|
165
|
+
}
|
|
166
|
+
resolve(userId);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get user identity - tries Google first, falls back to random ID
|
|
173
|
+
* @returns {Promise<{id: string, email: string | null, source: 'google' | 'random'}>}
|
|
174
|
+
*/
|
|
175
|
+
async function getUserIdentity() {
|
|
176
|
+
if (cachedIdentity) {
|
|
177
|
+
return cachedIdentity;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Try Google Identity first
|
|
181
|
+
const googleInfo = await getGoogleIdentity();
|
|
182
|
+
if (googleInfo && googleInfo.email) {
|
|
183
|
+
cachedIdentity = {
|
|
184
|
+
id: googleInfo.email,
|
|
185
|
+
email: googleInfo.email,
|
|
186
|
+
source: 'google'
|
|
187
|
+
};
|
|
188
|
+
console.log('PayVia: Using Google identity:', cachedIdentity.email);
|
|
189
|
+
return cachedIdentity;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Fall back to random ID
|
|
193
|
+
const randomId = await getRandomId();
|
|
194
|
+
cachedIdentity = {
|
|
195
|
+
id: randomId,
|
|
196
|
+
email: null,
|
|
197
|
+
source: 'random'
|
|
198
|
+
};
|
|
199
|
+
console.log('PayVia: Using random identity:', cachedIdentity.id);
|
|
200
|
+
return cachedIdentity;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Generate UUID v4
|
|
205
|
+
*/
|
|
206
|
+
function generateUUID() {
|
|
207
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
208
|
+
const r = Math.random() * 16 | 0;
|
|
209
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
210
|
+
return v.toString(16);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Make API request to PayVia server
|
|
216
|
+
*/
|
|
217
|
+
async function apiRequest(endpoint, options = {}) {
|
|
218
|
+
const response = await fetch(`${PAYVIA_API_URL}${endpoint}`, {
|
|
219
|
+
...options,
|
|
220
|
+
headers: {
|
|
221
|
+
'Content-Type': 'application/json',
|
|
222
|
+
'X-API-Key': apiKey,
|
|
223
|
+
...options.headers,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
const error = await response.json().catch(() => ({ error: 'Request failed' }));
|
|
229
|
+
throw new Error(error.error || 'PayVia API request failed');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return response.json();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get user's payment status
|
|
237
|
+
* Uses cache first, then server. Falls back to cache during network errors.
|
|
238
|
+
* @param {Object} options - Options
|
|
239
|
+
* @param {boolean} options.forceRefresh - Force fetch from server
|
|
240
|
+
* @returns {Promise<PayViaUser>} User object with payment status
|
|
241
|
+
*/
|
|
242
|
+
instance.getUser = async function (options = {}) {
|
|
243
|
+
const identity = await getUserIdentity();
|
|
244
|
+
|
|
245
|
+
// Check in-memory cache first
|
|
246
|
+
if (cachedUser && !options.forceRefresh) {
|
|
247
|
+
return cachedUser;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (userPromise) {
|
|
251
|
+
return userPromise;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
userPromise = (async () => {
|
|
255
|
+
try {
|
|
256
|
+
// Check persistent cache
|
|
257
|
+
if (!options.forceRefresh) {
|
|
258
|
+
const cached = await getCachedLicense();
|
|
259
|
+
if (cached && isCacheValid(cached)) {
|
|
260
|
+
cachedUser = buildUserFromCache(identity, cached, true);
|
|
261
|
+
return cachedUser;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Fetch from server
|
|
266
|
+
const response = await apiRequest('/api/v1/license/validate', {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
body: JSON.stringify({ customerId: identity.id, email: identity.email }),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Save to cache
|
|
272
|
+
const cacheData = {
|
|
273
|
+
status: response.status,
|
|
274
|
+
planIds: response.planIds || [],
|
|
275
|
+
tier: response.tier || null,
|
|
276
|
+
isTrial: response.isTrial || false,
|
|
277
|
+
trialExpiresAt: response.trialExpiresAt || null,
|
|
278
|
+
daysRemaining: response.daysRemaining || null,
|
|
279
|
+
checkedAt: response.checkedAt || Date.now(),
|
|
280
|
+
ttl: response.ttl || DEFAULT_TTL_MS,
|
|
281
|
+
signature: response.signature || null,
|
|
282
|
+
version: response.version || null,
|
|
283
|
+
};
|
|
284
|
+
await setCachedLicense(cacheData);
|
|
285
|
+
|
|
286
|
+
cachedUser = buildUserFromCache(identity, cacheData, false);
|
|
287
|
+
return cachedUser;
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error('PayVia: Failed to get user status', error);
|
|
290
|
+
|
|
291
|
+
// Try to use cached data if within grace period
|
|
292
|
+
const cached = await getCachedLicense();
|
|
293
|
+
if (cached && isWithinGracePeriod(cached)) {
|
|
294
|
+
console.log('PayVia: Using cached license (network error, within grace period)');
|
|
295
|
+
cachedUser = buildUserFromCache(identity, cached, true);
|
|
296
|
+
return cachedUser;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// No valid cache, return inactive
|
|
300
|
+
return {
|
|
301
|
+
id: identity.id,
|
|
302
|
+
email: identity.email,
|
|
303
|
+
identitySource: identity.source,
|
|
304
|
+
paid: false,
|
|
305
|
+
status: 'INACTIVE',
|
|
306
|
+
tier: null,
|
|
307
|
+
features: [],
|
|
308
|
+
planIds: [],
|
|
309
|
+
isTrial: false,
|
|
310
|
+
fromCache: false,
|
|
311
|
+
error: error.message,
|
|
312
|
+
};
|
|
313
|
+
} finally {
|
|
314
|
+
userPromise = null;
|
|
315
|
+
}
|
|
316
|
+
})();
|
|
317
|
+
|
|
318
|
+
return userPromise;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Build user object from cache data
|
|
323
|
+
*/
|
|
324
|
+
function buildUserFromCache(identity, cache, fromCache) {
|
|
325
|
+
return {
|
|
326
|
+
id: identity.id,
|
|
327
|
+
email: identity.email,
|
|
328
|
+
identitySource: identity.source,
|
|
329
|
+
paid: cache.status === 'ACTIVE' || cache.status === 'TRIAL',
|
|
330
|
+
status: cache.status,
|
|
331
|
+
tier: cache.tier || null,
|
|
332
|
+
features: cache.tier?.features || [],
|
|
333
|
+
planIds: cache.planIds || [],
|
|
334
|
+
isTrial: cache.isTrial || false,
|
|
335
|
+
trialExpiresAt: cache.trialExpiresAt ? new Date(cache.trialExpiresAt) : null,
|
|
336
|
+
daysRemaining: cache.daysRemaining || null,
|
|
337
|
+
fromCache: fromCache,
|
|
338
|
+
checkedAt: cache.checkedAt || null,
|
|
339
|
+
ttl: cache.ttl || null,
|
|
340
|
+
signature: cache.signature || null,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Force refresh user status from server
|
|
346
|
+
* @returns {Promise<PayViaUser>} Updated user object
|
|
347
|
+
*/
|
|
348
|
+
instance.refresh = async function () {
|
|
349
|
+
cachedUser = null;
|
|
350
|
+
return instance.getUser({ forceRefresh: true });
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Refresh license cache from server (for background refresh)
|
|
355
|
+
* @returns {Promise<void>}
|
|
356
|
+
*/
|
|
357
|
+
instance.refreshLicenseCache = async function () {
|
|
358
|
+
const cached = await getCachedLicense();
|
|
359
|
+
if (!cached || !isCacheValid(cached)) {
|
|
360
|
+
await instance.refresh();
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Check if user has a specific feature
|
|
366
|
+
* @param {string} featureName - Feature name to check
|
|
367
|
+
* @returns {Promise<boolean>}
|
|
368
|
+
*/
|
|
369
|
+
instance.hasFeature = async function (featureName) {
|
|
370
|
+
const user = await instance.getUser();
|
|
371
|
+
if (!user.tier || !user.tier.features) return false;
|
|
372
|
+
return user.tier.features.includes(featureName);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Check if user has a tier at or above the required level
|
|
377
|
+
* @param {number} requiredLevel - Minimum tier level required
|
|
378
|
+
* @returns {Promise<boolean>}
|
|
379
|
+
*/
|
|
380
|
+
instance.hasTierLevel = async function (requiredLevel) {
|
|
381
|
+
const user = await instance.getUser();
|
|
382
|
+
if (!user.tier) return false;
|
|
383
|
+
return user.tier.level >= requiredLevel;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Get the user's current tier info
|
|
388
|
+
* @returns {Promise<{id: string, name: string, level: number, features: string[]}|null>}
|
|
389
|
+
*/
|
|
390
|
+
instance.getTier = async function () {
|
|
391
|
+
const user = await instance.getUser();
|
|
392
|
+
return user.tier || null;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Start a trial for the current user
|
|
397
|
+
* Call this when user first installs/uses the extension
|
|
398
|
+
* Idempotent: if user already has a trial/active subscription, returns existing info
|
|
399
|
+
* @returns {Promise<{subscriptionId: string, status: string, planId: string, planName: string, trialExpiresAt: Date, daysRemaining: number} | null>}
|
|
400
|
+
*/
|
|
401
|
+
instance.startTrial = async function () {
|
|
402
|
+
try {
|
|
403
|
+
const identity = await getUserIdentity();
|
|
404
|
+
const response = await apiRequest('/api/v1/trial/start', {
|
|
405
|
+
method: 'POST',
|
|
406
|
+
body: JSON.stringify({
|
|
407
|
+
customerId: identity.id,
|
|
408
|
+
email: identity.email,
|
|
409
|
+
}),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Clear cached user so next getUser() fetches fresh data
|
|
413
|
+
cachedUser = null;
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
subscriptionId: response.subscriptionId,
|
|
417
|
+
status: response.status,
|
|
418
|
+
planId: response.planId,
|
|
419
|
+
planName: response.planName,
|
|
420
|
+
trialExpiresAt: new Date(response.trialExpiresAt),
|
|
421
|
+
daysRemaining: response.daysRemaining,
|
|
422
|
+
};
|
|
423
|
+
} catch (error) {
|
|
424
|
+
console.log('PayVia: Could not start trial:', error.message);
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get trial status for the current user
|
|
431
|
+
* @returns {Promise<{status: string, trialExpiresAt: Date | null, daysRemaining: number | null, canConvert: boolean, planIds: string[]}>}
|
|
432
|
+
*/
|
|
433
|
+
instance.getTrialStatus = async function () {
|
|
434
|
+
try {
|
|
435
|
+
const identity = await getUserIdentity();
|
|
436
|
+
const response = await apiRequest('/api/v1/trial/status', {
|
|
437
|
+
method: 'POST',
|
|
438
|
+
body: JSON.stringify({ customerId: identity.id }),
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
status: response.status,
|
|
443
|
+
trialExpiresAt: response.trialExpiresAt ? new Date(response.trialExpiresAt) : null,
|
|
444
|
+
daysRemaining: response.daysRemaining || null,
|
|
445
|
+
canConvert: response.canConvert,
|
|
446
|
+
planIds: response.planIds || [],
|
|
447
|
+
};
|
|
448
|
+
} catch (error) {
|
|
449
|
+
console.error('PayVia: Failed to get trial status', error);
|
|
450
|
+
return {
|
|
451
|
+
status: 'UNKNOWN',
|
|
452
|
+
trialExpiresAt: null,
|
|
453
|
+
daysRemaining: null,
|
|
454
|
+
canConvert: true,
|
|
455
|
+
planIds: [],
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Check if this is the first time the extension is being used
|
|
462
|
+
* @returns {Promise<boolean>}
|
|
463
|
+
*/
|
|
464
|
+
instance.isFirstRun = async function () {
|
|
465
|
+
return new Promise((resolve) => {
|
|
466
|
+
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
|
|
467
|
+
chrome.storage.local.get(['payvia_first_run_done'], (result) => {
|
|
468
|
+
resolve(!result.payvia_first_run_done);
|
|
469
|
+
});
|
|
470
|
+
} else {
|
|
471
|
+
resolve(!localStorage.getItem('payvia_first_run_done'));
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Mark first run as complete
|
|
478
|
+
*/
|
|
479
|
+
instance.markFirstRunDone = async function () {
|
|
480
|
+
return new Promise((resolve) => {
|
|
481
|
+
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
|
|
482
|
+
chrome.storage.local.set({ payvia_first_run_done: true }, resolve);
|
|
483
|
+
} else {
|
|
484
|
+
localStorage.setItem('payvia_first_run_done', 'true');
|
|
485
|
+
resolve();
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Check if email is required for payment (user has no Google identity)
|
|
492
|
+
* @returns {Promise<boolean>}
|
|
493
|
+
*/
|
|
494
|
+
instance.needsEmailForPayment = async function () {
|
|
495
|
+
const identity = await getUserIdentity();
|
|
496
|
+
return identity.source === 'random';
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Get the current user identity info
|
|
501
|
+
* @returns {Promise<{id: string, email: string | null, source: 'google' | 'random'}>}
|
|
502
|
+
*/
|
|
503
|
+
instance.getIdentity = async function () {
|
|
504
|
+
return getUserIdentity();
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Open payment page for the user
|
|
509
|
+
* @param {Object} options - Payment options
|
|
510
|
+
* @param {string} options.planId - Plan ID to purchase (required for direct/hosted modes)
|
|
511
|
+
* @param {string} options.email - Customer email
|
|
512
|
+
* @param {'pricing'|'hosted'|'direct'} options.mode - Checkout mode:
|
|
513
|
+
* - 'pricing': Shows all plans for user to choose (default, most secure)
|
|
514
|
+
* - 'hosted': Goes directly to checkout for specific plan
|
|
515
|
+
* - 'direct': Bypasses PayVia, goes straight to PayPal
|
|
516
|
+
* @param {string} options.successUrl - URL to redirect after successful payment (direct mode only)
|
|
517
|
+
* @param {string} options.cancelUrl - URL to redirect if user cancels (direct mode only)
|
|
518
|
+
*/
|
|
519
|
+
instance.openPaymentPage = async function (options = {}) {
|
|
520
|
+
const identity = await getUserIdentity();
|
|
521
|
+
const customerEmail = options.email || identity.email;
|
|
522
|
+
const mode = options.mode || 'pricing';
|
|
523
|
+
|
|
524
|
+
const PAYVIA_BASE_URL = 'https://payvia.site';
|
|
525
|
+
|
|
526
|
+
if (mode === 'pricing') {
|
|
527
|
+
// Pricing mode: Get secure token, show all plans
|
|
528
|
+
if (!customerEmail) {
|
|
529
|
+
throw new Error('Email is required for payment. Please provide an email address.');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const tokenResponse = await apiRequest('/api/v1/checkout/token', {
|
|
533
|
+
method: 'POST',
|
|
534
|
+
body: JSON.stringify({
|
|
535
|
+
customerId: identity.id,
|
|
536
|
+
customerEmail: customerEmail,
|
|
537
|
+
mode: 'pricing',
|
|
538
|
+
}),
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const pricingUrl = `${PAYVIA_BASE_URL}/pricing?token=${encodeURIComponent(tokenResponse.token)}`;
|
|
542
|
+
|
|
543
|
+
if (typeof chrome !== 'undefined' && chrome.tabs) {
|
|
544
|
+
chrome.tabs.create({ url: pricingUrl });
|
|
545
|
+
} else {
|
|
546
|
+
window.open(pricingUrl, '_blank');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return { mode: 'pricing', pricingUrl };
|
|
550
|
+
} else if (mode === 'hosted') {
|
|
551
|
+
// Hosted mode: Get secure token, go to specific plan checkout
|
|
552
|
+
if (!options.planId) {
|
|
553
|
+
throw new Error('planId is required for hosted mode');
|
|
554
|
+
}
|
|
555
|
+
if (!customerEmail) {
|
|
556
|
+
throw new Error('Email is required for payment. Please provide an email address.');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const tokenResponse = await apiRequest('/api/v1/checkout/token', {
|
|
560
|
+
method: 'POST',
|
|
561
|
+
body: JSON.stringify({
|
|
562
|
+
customerId: identity.id,
|
|
563
|
+
customerEmail: customerEmail,
|
|
564
|
+
planId: options.planId,
|
|
565
|
+
mode: 'checkout',
|
|
566
|
+
}),
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const checkoutUrl = `${PAYVIA_BASE_URL}/checkout?token=${encodeURIComponent(tokenResponse.token)}`;
|
|
570
|
+
|
|
571
|
+
if (typeof chrome !== 'undefined' && chrome.tabs) {
|
|
572
|
+
chrome.tabs.create({ url: checkoutUrl });
|
|
573
|
+
} else {
|
|
574
|
+
window.open(checkoutUrl, '_blank');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return { mode: 'hosted', checkoutUrl };
|
|
578
|
+
} else {
|
|
579
|
+
// Direct mode: Call API and redirect straight to PayPal (original behavior)
|
|
580
|
+
if (!options.planId) {
|
|
581
|
+
throw new Error('planId is required for direct mode');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
const response = await apiRequest('/api/v1/checkout-session', {
|
|
586
|
+
method: 'POST',
|
|
587
|
+
body: JSON.stringify({
|
|
588
|
+
planId: options.planId,
|
|
589
|
+
customerId: identity.id,
|
|
590
|
+
customerEmail: customerEmail || '',
|
|
591
|
+
successUrl: options.successUrl || 'https://payvia.site/success',
|
|
592
|
+
cancelUrl: options.cancelUrl || 'https://payvia.site/cancel',
|
|
593
|
+
}),
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// Open PayPal checkout in new tab
|
|
597
|
+
if (response.checkoutUrl) {
|
|
598
|
+
if (typeof chrome !== 'undefined' && chrome.tabs) {
|
|
599
|
+
chrome.tabs.create({ url: response.checkoutUrl });
|
|
600
|
+
} else {
|
|
601
|
+
window.open(response.checkoutUrl, '_blank');
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return response;
|
|
606
|
+
} catch (error) {
|
|
607
|
+
console.error('PayVia: Failed to open payment page', error);
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Listen for payment status changes
|
|
615
|
+
* @param {Function} callback - Called when payment status changes
|
|
616
|
+
*/
|
|
617
|
+
instance.onPaid = function (callback) {
|
|
618
|
+
// Poll for status changes every 5 seconds when tab is visible
|
|
619
|
+
let lastPaidStatus = null;
|
|
620
|
+
|
|
621
|
+
const checkStatus = async () => {
|
|
622
|
+
const user = await instance.refresh();
|
|
623
|
+
if (lastPaidStatus !== null && lastPaidStatus !== user.paid) {
|
|
624
|
+
callback(user);
|
|
625
|
+
}
|
|
626
|
+
lastPaidStatus = user.paid;
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// Check immediately
|
|
630
|
+
checkStatus();
|
|
631
|
+
|
|
632
|
+
// Set up polling
|
|
633
|
+
const intervalId = setInterval(checkStatus, 5000);
|
|
634
|
+
|
|
635
|
+
// Return cleanup function
|
|
636
|
+
return () => clearInterval(intervalId);
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Get available plans for this project
|
|
641
|
+
* @returns {Promise<Plan[]>} List of available plans
|
|
642
|
+
*/
|
|
643
|
+
instance.getPlans = async function () {
|
|
644
|
+
try {
|
|
645
|
+
const response = await apiRequest('/api/v1/plans');
|
|
646
|
+
return response;
|
|
647
|
+
} catch (error) {
|
|
648
|
+
console.error('PayVia: Failed to get plans', error);
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Reset the user's license (delete all subscriptions).
|
|
655
|
+
* This is for demo/testing purposes only.
|
|
656
|
+
* @returns {Promise<{message: string}>}
|
|
657
|
+
*/
|
|
658
|
+
instance.resetLicense = async function () {
|
|
659
|
+
const identity = await getUserIdentity();
|
|
660
|
+
const response = await apiRequest('/api/v1/license/reset', {
|
|
661
|
+
method: 'POST',
|
|
662
|
+
body: JSON.stringify({ customerId: identity.id }),
|
|
663
|
+
});
|
|
664
|
+
cachedUser = null;
|
|
665
|
+
return response;
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Cancel a subscription for the current user
|
|
670
|
+
* @param {Object} options - Cancel options
|
|
671
|
+
* @param {string} options.planId - Specific plan to cancel (optional)
|
|
672
|
+
* @param {string} options.reason - Cancellation reason (optional)
|
|
673
|
+
* @returns {Promise<{success: boolean, message: string, canceledPlanId: string}>}
|
|
674
|
+
*/
|
|
675
|
+
instance.cancelSubscription = async function (options = {}) {
|
|
676
|
+
const identity = await getUserIdentity();
|
|
677
|
+
|
|
678
|
+
const response = await apiRequest('/api/v1/license/cancel', {
|
|
679
|
+
method: 'POST',
|
|
680
|
+
body: JSON.stringify({
|
|
681
|
+
customerId: identity.id,
|
|
682
|
+
planId: options.planId || null,
|
|
683
|
+
reason: options.reason || 'User requested cancellation',
|
|
684
|
+
}),
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// Clear cached user so next getUser() fetches fresh data
|
|
688
|
+
cachedUser = null;
|
|
689
|
+
|
|
690
|
+
return response;
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
return instance;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export default PayVia;
|