/****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; class RefreshScheduler { constructor(iapticStripe) { this.schedules = []; this.iapticStripe = iapticStripe; } setTimeout(schedule) { const delay = schedule.scheduledAt - Date.now(); // console.log(`Scheduling refresh for ${schedule.subscriptionId} (${schedule.reason}) in ${delay}ms`); if (delay <= 0) return; const SET_TIMEOUT_MAX_DELAY = 2147483647; if (delay > SET_TIMEOUT_MAX_DELAY) { // console.log(`Scheduled refresh for ${schedule.subscriptionId} (${schedule.reason}) is too far in the future: ${delay}ms`); return; } setTimeout(() => __awaiter(this, void 0, void 0, function* () { if (schedule.completed) return; if (schedule.scheduledAt - 10000 > Date.now()) return; // something went wrong // console.log(`Running refresh for ${schedule.subscriptionId} (${schedule.reason})`); const inProgressRefresh = this.schedules.find(s => s.subscriptionId === schedule.subscriptionId && s.inProgress); if (inProgressRefresh) { console.log(`Skipping refresh for ${schedule.subscriptionId} (${schedule.reason}): another refresh in progress`); return; } try { console.log(`Refreshing subscription ${schedule.subscriptionId} (${schedule.reason})`); schedule.inProgress = true; yield this.iapticStripe.getPurchases(); schedule.completed = true; } catch (error) { console.error('Error refreshing subscription:', error); if (!schedule.reason.startsWith('retry-')) { const retryDate = new Date(Date.now() + 30000); this.scheduleRefresh(schedule.subscriptionId, retryDate, `retry-${schedule.reason}`); } } finally { schedule.inProgress = false; } }), delay); } scheduleRefresh(subscriptionId, date, reason) { const schedule = { id: `${subscriptionId}-${date.getTime()}`, subscriptionId, scheduledAt: date.getTime(), completed: false, inProgress: false, reason }; if (this.schedules.some(s => s.id === schedule.id)) { return; } this.schedules.push(schedule); this.setTimeout(schedule); } schedulePurchaseRefreshes(purchase) { if (!purchase.expirationDate) { return; } const expirationDate = new Date(purchase.expirationDate); // console.log(`Subscription ${purchase.purchaseId} expiration date: ${expirationDate.toISOString()}`); const beforeExpiration = new Date(expirationDate.getTime() - 10000); const afterExpiration = new Date(expirationDate.getTime() + 10000); const dates = []; dates.push({ date: beforeExpiration, reason: 'pre-expiration' }); dates.push({ date: afterExpiration, reason: 'post-expiration' }); dates.forEach(({ date, reason }) => { if (date.getTime() > Date.now()) { this.scheduleRefresh(purchase.purchaseId, date, reason); } }); } clearSchedules() { this.schedules = []; } } /** * Utility functions for the Iaptic library */ class Utils { /** * Base64 encode a string * @param str String to encode */ static base64Encode(str) { try { return btoa(str); } catch (e) { // Fallback for older browsers or non-ASCII characters // Use Buffer for Node.js environments if (typeof Buffer !== 'undefined') { return Buffer.from(str).toString('base64'); } // Use TextEncoder for modern browsers if (typeof TextEncoder !== 'undefined') { const bytes = new TextEncoder().encode(str); const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join(''); return btoa(binString); } // Basic fallback return btoa(encodeURIComponent(str)); } } /** * Get item from localStorage with type safety * * @param key Storage key * @param defaultValue Default value if not found */ static storageGetJson(key) { try { const value = localStorage.getItem(key); return value !== null ? JSON.parse(value) : null; } catch (e) { console.error('Error reading from localStorage:', e); return null; } } /** * Get item from localStorage as string * * @param key Storage key */ static storageGetString(key) { const value = localStorage.getItem(key); return value !== null ? value : null; } /** * Set item in localStorage * * @param key Storage key * @param value Value to store */ static storageSetJson(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (e) { console.error('Error writing to localStorage:', e); return false; } } static storageSetString(key, value) { try { localStorage.setItem(key, value); return true; } catch (e) { console.error('Error writing to localStorage:', e); return false; } } /** * Remove item from storage * * @param key Storage key */ static storageRemove(key) { try { localStorage.removeItem(key); return true; } catch (e) { console.error('Error removing from localStorage:', e); return false; } } /** * Build URL with query parameters * @param baseUrl Base URL * @param params Query parameters */ static buildUrl(baseUrl, params) { try { // Remove trailing slash from baseUrl const cleanBaseUrl = baseUrl.replace(/\/$/, ''); // If no params, return clean URL if (Object.keys(params).length === 0) { return cleanBaseUrl; } // Build query string const query = Object.entries(params) .filter(([_, value]) => value !== undefined && value !== null) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); return query ? `${cleanBaseUrl}?${query}` : cleanBaseUrl; } catch (e) { // Fallback is now the same as the main implementation const cleanBaseUrl = baseUrl.replace(/\/$/, ''); if (Object.keys(params).length === 0) { return cleanBaseUrl; } const query = Object.entries(params) .filter(([_, value]) => value !== undefined && value !== null) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); return query ? `${cleanBaseUrl}?${query}` : cleanBaseUrl; } } /** * Format a price amount from micros */ static formatCurrency(amountMicros, currency) { if (typeof amountMicros !== 'number' || typeof currency !== 'string') { return ''; } currency = currency.toUpperCase(); try { const amount = amountMicros / 1000000; return new Intl.NumberFormat(undefined, { style: 'currency', currency: currency }).format(amount).replace('.00', ''); } catch (error) { // Fallback formatting for common currencies const amount = amountMicros / 1000000; const currencyFormats = { USD: { symbol: '$', position: 'before' }, EUR: { symbol: '€', position: 'before' }, GBP: { symbol: '£', position: 'before' }, JPY: { symbol: '¥', position: 'before' }, CNY: { symbol: '¥', position: 'before' }, KRW: { symbol: '₩', position: 'before' }, INR: { symbol: '₹', position: 'before' }, RUB: { symbol: '₽', position: 'after' }, BRL: { symbol: 'R$', position: 'before' }, CHF: { symbol: 'CHF', position: 'before' }, CAD: { symbol: 'CA$', position: 'before' }, AUD: { symbol: 'A$', position: 'before' }, NZD: { symbol: 'NZ$', position: 'before' }, HKD: { symbol: 'HK$', position: 'before' }, SGD: { symbol: 'S$', position: 'before' }, SEK: { symbol: 'kr', position: 'after' }, NOK: { symbol: 'kr', position: 'after' }, DKK: { symbol: 'kr', position: 'after' }, PLN: { symbol: 'zł', position: 'after' } }; const format = currencyFormats[currency]; if (format) { const formattedAmount = amount.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 }); return (format.position === 'before' ? `${format.symbol}${formattedAmount}` : `${formattedAmount} ${format.symbol}`).replace('.00', ''); } // Default fallback for unknown currencies return `${currency} ${amount.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 })}`.replace('.00', ''); } } /** * Format a ISO 8601 period in English * * @param period ISO 8601 period */ static formatBillingPeriodEN(period) { if (!period) return ''; const match = period.match(/P(\d+)([YMWD])/); if (!match) return period; const [_, count, unit] = match; const displayCount = count === '1' ? '' : ' ' + count; switch (unit) { case 'Y': return count === '1' ? 'Yearly' : `Every${displayCount} years`; case 'M': return count === '1' ? 'Monthly' : `Every${displayCount} months`; case 'W': return count === '1' ? 'Weekly' : `Every${displayCount} weeks`; case 'D': return count === '1' ? 'Daily' : `Every${displayCount} days`; default: return period; } } } class IapticStripe { constructor(config) { var _a; if (config.type !== 'stripe') { throw new Error('Unsupported adapter type'); } if (!config.stripePublicKey) { throw new Error('Missing required Stripe public key'); } if (!config.appName || !config.apiKey) { throw new Error('Missing required Iaptic configuration'); } this.iapticUrl = ((_a = config.customIapticUrl) === null || _a === void 0 ? void 0 : _a.replace(/\/$/, '')) || 'https://validator.iaptic.com'; this.appName = config.appName; this.apiKey = config.apiKey; this.refreshScheduler = new RefreshScheduler(this); } authorizationHeader() { return `Basic ${Utils.base64Encode(`${this.appName}:${this.apiKey}`)}`; } getProducts() { return __awaiter(this, void 0, void 0, function* () { const cached = this._getCachedProducts(); if (cached === null || cached === void 0 ? void 0 : cached.products) { return cached.products; } return this.refreshProducts(); }); } refreshProducts() { return __awaiter(this, void 0, void 0, function* () { // Check if we have very recent cached data (less than 1 minute old) const cached = this._getCachedProducts(); if ((cached === null || cached === void 0 ? void 0 : cached.products) && cached.fetchedAt > Date.now() - 60000) { return cached.products; } try { const response = yield fetch(`${this.iapticUrl}/v3/stripe/prices`, { headers: { Authorization: this.authorizationHeader() } }); if (!response.ok) { throw new Error('Failed to fetch prices from Iaptic'); } const data = yield response.json(); if (!data.ok || !data.products) { throw new Error('Invalid response from Iaptic'); } // Update cache this._setCachedProducts(data.products); return data.products; } catch (error) { console.error('Error fetching prices:', error); throw error; } }); } getAccessToken() { return this._getStoredAccessToken(); } order(params) { return __awaiter(this, void 0, void 0, function* () { if (!params.accessToken) { params.accessToken = this._getStoredAccessToken(); } try { const response = yield fetch(`${this.iapticUrl}/v3/stripe/checkout`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: this.authorizationHeader() }, body: JSON.stringify(params) }); if (!response.ok) { throw new Error('Failed to create checkout session'); } const data = yield response.json(); if (!data.ok || !data.url) { throw new Error('Invalid checkout session response'); } // Store session ID and access token if (data.accessToken) { this._storeAccessToken(data.accessToken); } // Redirect to Stripe Checkout window.location.href = data.url; } catch (error) { console.error('Error creating checkout session:', error); throw error; } }); } /** * Get purchases status. * * By default, it will use the access token stored in the browser's localStorage. You can * pass an optional access token to get the purchases for a specific user. * * @param accessToken Optional access token for the user */ getPurchases(accessToken) { return __awaiter(this, void 0, void 0, function* () { if (!accessToken) { accessToken = this._getStoredAccessToken(); } if (!accessToken) { return []; } try { const response = yield fetch(Utils.buildUrl(`${this.iapticUrl}/v3/stripe/purchases`, { accessToken: accessToken }), { headers: { Authorization: this.authorizationHeader() } }); if (!response.ok) { const error = yield response.json(); throw new Error(error.message || 'Failed to fetch purchases'); } const data = yield response.json(); if (!data.ok) { throw new Error('Invalid purchases response'); } // Schedule refreshes for each purchase data.purchases.forEach(purchase => { this.refreshScheduler.schedulePurchaseRefreshes(purchase); }); // Store new access token if provided if (data.newAccessToken) { this._storeAccessToken(data.newAccessToken); } return data.purchases; } catch (error) { console.error('Error fetching purchases:', error); throw error; } }); } /** * Redirects to Stripe Customer Portal for subscription management */ redirectToCustomerPortal(params) { return __awaiter(this, void 0, void 0, function* () { if (!params.accessToken) { params.accessToken = this._getStoredAccessToken(); } if (!params.accessToken) { throw new Error('No access token available'); } if (!params.returnUrl) { params.returnUrl = window.location.href; } try { const response = yield fetch(`${this.iapticUrl}/v3/stripe/portal`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: this.authorizationHeader() }, body: JSON.stringify({ returnUrl: params.returnUrl, accessToken: params.accessToken }) }); if (!response.ok) { const error = yield response.json(); throw new Error(error.message || 'Failed to create portal session'); } const data = yield response.json(); if (!data.ok || !data.url) { throw new Error('Invalid portal session response'); } // Redirect to the customer portal window.location.href = data.url; } catch (error) { console.error('Error redirecting to customer portal:', error); throw error; } }); } /** * Changes the subscription plan * * @param {PlanChange} planChange - Plan change request * * @returns {Promise} Updated purchase details */ changePlan(planChange) { return __awaiter(this, void 0, void 0, function* () { if (!planChange.accessToken) { planChange.accessToken = this._getStoredAccessToken(); } if (!planChange.accessToken) { throw new Error('No access token available'); } try { const response = yield fetch(`${this.iapticUrl}/v3/stripe/change-plan`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': this.authorizationHeader() }, body: JSON.stringify(planChange) }); if (!response.ok) { const error = yield response.json(); throw new Error(error.message || 'Failed to change plan'); } const data = yield response.json(); if (!data.ok || !data.purchase) { throw new Error('Invalid change plan response'); } // Store any new access keys if (data.newAccessToken) { this._storeAccessToken(data.newAccessToken); } // Schedule refreshes for the updated purchase this.refreshScheduler.schedulePurchaseRefreshes(data.purchase); // Also schedule an immediate refresh to catch any quick changes this.refreshScheduler.scheduleRefresh(data.purchase.purchaseId, new Date(Date.now() + 10000), // 10 seconds from now 'post-change-verification'); return data.purchase; } catch (error) { console.error('Error changing plan:', error); throw error; } }); } // // Storage // clearStoredData() { Utils.storageRemove('iaptic_access_token'); Utils.storageRemove('iaptic_products'); this.refreshScheduler.clearSchedules(); } _storeAccessToken(accessToken) { try { Utils.storageSetString('iaptic_access_token', accessToken); } catch (error) { console.error('Error storing access token:', error); } } _getStoredAccessToken() { var _a; return (_a = Utils.storageGetString('iaptic_access_token')) !== null && _a !== void 0 ? _a : undefined; } _getCachedProducts() { return Utils.storageGetJson('iaptic_products'); } _setCachedProducts(products) { Utils.storageSetJson('iaptic_products', { products, fetchedAt: Date.now() }); } } IapticStripe.VERSION = '1.0.0'; function createAdapter(config) { if (config.type === 'stripe') { return new IapticStripe(config); } throw new Error('Unsupported adapter type'); } const IapticJS = { createAdapter, }; export { IapticJS, IapticStripe, RefreshScheduler, Utils, createAdapter, IapticJS as default };