@virex-tech/paywallo-sdk 1.1.0 → 1.1.3

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/dist/index.js CHANGED
@@ -1030,6 +1030,13 @@ function getWebViewEmitter() {
1030
1030
  }
1031
1031
  return webViewEmitter;
1032
1032
  }
1033
+ function parseErrorEvent(raw) {
1034
+ return {
1035
+ code: typeof raw["code"] === "number" ? raw["code"] : -1,
1036
+ description: typeof raw["description"] === "string" ? raw["description"] : "WebView error",
1037
+ url: typeof raw["url"] === "string" ? raw["url"] : void 0
1038
+ };
1039
+ }
1033
1040
  function PaywallWebView({
1034
1041
  paywallId,
1035
1042
  craftData,
@@ -1040,11 +1047,14 @@ function PaywallWebView({
1040
1047
  onPurchase,
1041
1048
  onClose,
1042
1049
  onRestore,
1043
- onReady
1050
+ onReady,
1051
+ onError
1044
1052
  }) {
1045
1053
  const paywallDataSent = (0, import_react4.useRef)(false);
1046
1054
  const loadEndReceived = (0, import_react4.useRef)(false);
1047
1055
  const [scriptToInject, setScriptToInject] = (0, import_react4.useState)(void 0);
1056
+ const onErrorRef = (0, import_react4.useRef)(onError);
1057
+ onErrorRef.current = onError;
1048
1058
  const NativeWebView = (0, import_react4.useMemo)(() => getNativeWebView(), []);
1049
1059
  const webUrl = (0, import_react4.useMemo)(() => PaywalloClient.getWebUrl() ?? "http://localhost:3000", []);
1050
1060
  const buildAndSetInjectionScript = (0, import_react4.useCallback)(() => {
@@ -1104,17 +1114,51 @@ function PaywallWebView({
1104
1114
  const message = parseWebViewMessage(event.data);
1105
1115
  if (message) handleParsedMessage(message);
1106
1116
  });
1117
+ const errorSub = emitter.addListener(
1118
+ "PaywalloWebViewError",
1119
+ (event) => {
1120
+ onErrorRef.current?.(parseErrorEvent(event));
1121
+ }
1122
+ );
1107
1123
  return () => {
1108
1124
  loadEndSub.remove();
1109
1125
  messageSub.remove();
1126
+ errorSub.remove();
1110
1127
  };
1111
1128
  }, [buildAndSetInjectionScript, handleParsedMessage]);
1129
+ (0, import_react4.useEffect)(() => {
1130
+ const timer = setTimeout(() => {
1131
+ if (!paywallDataSent.current) {
1132
+ onErrorRef.current?.({
1133
+ code: -1001,
1134
+ description: "Paywall load timed out after 10 seconds"
1135
+ });
1136
+ }
1137
+ }, 1e4);
1138
+ return () => clearTimeout(timer);
1139
+ }, []);
1112
1140
  const handleLoadEnd = (0, import_react4.useCallback)(() => {
1113
1141
  if (!loadEndReceived.current) {
1114
1142
  loadEndReceived.current = true;
1115
1143
  buildAndSetInjectionScript();
1116
1144
  }
1117
1145
  }, [buildAndSetInjectionScript]);
1146
+ const handleNativeError = (0, import_react4.useCallback)((event) => {
1147
+ const ne = event.nativeEvent;
1148
+ let errorData;
1149
+ if (ne["payload"] && typeof ne["payload"] === "object") {
1150
+ errorData = ne["payload"];
1151
+ } else if (typeof ne["data"] === "string") {
1152
+ try {
1153
+ errorData = JSON.parse(ne["data"]);
1154
+ } catch {
1155
+ errorData = {};
1156
+ }
1157
+ } else {
1158
+ errorData = {};
1159
+ }
1160
+ onErrorRef.current?.(parseErrorEvent(errorData));
1161
+ }, []);
1118
1162
  const handleMessage = (0, import_react4.useCallback)(
1119
1163
  (event) => {
1120
1164
  const message = parseWebViewMessage(event.nativeEvent.data);
@@ -1134,6 +1178,7 @@ function PaywallWebView({
1134
1178
  injectedJavaScript: scriptToInject,
1135
1179
  onMessage: handleMessage,
1136
1180
  onLoadEnd: handleLoadEnd,
1181
+ onError: handleNativeError,
1137
1182
  style: styles.webview
1138
1183
  }
1139
1184
  ) });
@@ -1164,6 +1209,25 @@ function PaywallModal({
1164
1209
  campaignId
1165
1210
  );
1166
1211
  const { isPurchasing, slideAnim, handleClose, handlePurchase, handleRestore } = usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId);
1212
+ const [webViewError, setWebViewError] = (0, import_react5.useState)(null);
1213
+ const [retryCount, setRetryCount] = (0, import_react5.useState)(0);
1214
+ const retryCountRef = (0, import_react5.useRef)(0);
1215
+ const handleWebViewError = (0, import_react5.useCallback)(
1216
+ (err) => {
1217
+ if (retryCountRef.current >= 1) {
1218
+ setWebViewError({ code: err.code, description: err.description });
1219
+ } else {
1220
+ retryCountRef.current += 1;
1221
+ setRetryCount(retryCountRef.current);
1222
+ }
1223
+ },
1224
+ []
1225
+ );
1226
+ const handleRetry = (0, import_react5.useCallback)(() => {
1227
+ retryCountRef.current = 0;
1228
+ setWebViewError(null);
1229
+ setRetryCount(0);
1230
+ }, []);
1167
1231
  const openAnim = (0, import_react5.useRef)(new import_react_native5.Animated.Value(SCREEN_HEIGHT2)).current;
1168
1232
  (0, import_react5.useEffect)(() => {
1169
1233
  if (visible) {
@@ -1188,7 +1252,12 @@ function PaywallModal({
1188
1252
  statusBarTranslucent: true,
1189
1253
  children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.View, { style: styles2.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Animated.View, { style: [styles2.animatedContainer, { transform: [{ translateY: openAnim }] }], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native5.View, { style: styles2.container, children: [
1190
1254
  error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.View, { style: styles2.errorContainer, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Text, { style: styles2.errorText, children: error.message }) }),
1191
- !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1255
+ !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: webViewError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.View, { style: styles2.fallbackContainer, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native5.View, { style: styles2.fallbackCard, children: [
1256
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Text, { style: styles2.fallbackTitle, children: "Algo deu errado" }),
1257
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Text, { style: styles2.fallbackDescription, children: webViewError.description }),
1258
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Pressable, { style: styles2.retryButton, onPress: handleRetry, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Text, { style: styles2.retryButtonText, children: "Tentar novamente" }) }),
1259
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Pressable, { style: styles2.closeButton, onPress: handleClose, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native5.Text, { style: styles2.closeButtonText, children: "Fechar" }) })
1260
+ ] }) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1192
1261
  PaywallWebView,
1193
1262
  {
1194
1263
  paywallId: paywallConfig.id,
@@ -1199,39 +1268,67 @@ function PaywallModal({
1199
1268
  isPurchasing,
1200
1269
  onClose: handleClose,
1201
1270
  onPurchase: handlePurchase,
1202
- onRestore: handleRestore
1203
- }
1204
- )
1271
+ onRestore: handleRestore,
1272
+ onError: handleWebViewError
1273
+ },
1274
+ retryCount
1275
+ ) })
1205
1276
  ] }) }) })
1206
1277
  }
1207
1278
  );
1208
1279
  }
1209
1280
  var styles2 = import_react_native5.StyleSheet.create({
1210
- overlay: {
1281
+ overlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.3)" },
1282
+ animatedContainer: { flex: 1, backgroundColor: "#fff" },
1283
+ container: { flex: 1, backgroundColor: "#fff" },
1284
+ errorContainer: { flex: 1, justifyContent: "center", alignItems: "center", padding: 20 },
1285
+ errorText: { fontSize: 16, color: "#ef4444", textAlign: "center" },
1286
+ fallbackContainer: {
1211
1287
  flex: 1,
1212
- backgroundColor: "rgba(0,0,0,0.3)"
1288
+ justifyContent: "center",
1289
+ alignItems: "center",
1290
+ backgroundColor: "#111010",
1291
+ padding: 24
1213
1292
  },
1214
- animatedContainer: {
1215
- flex: 1,
1216
- backgroundColor: "#fff",
1217
- borderTopLeftRadius: 0,
1218
- borderTopRightRadius: 0
1293
+ fallbackCard: {
1294
+ width: "100%",
1295
+ maxWidth: 360,
1296
+ alignItems: "center",
1297
+ backgroundColor: "rgba(255,255,255,0.07)",
1298
+ borderRadius: 16,
1299
+ padding: 28
1219
1300
  },
1220
- container: {
1221
- flex: 1,
1222
- backgroundColor: "#fff"
1301
+ fallbackTitle: {
1302
+ fontSize: 18,
1303
+ fontWeight: "600",
1304
+ color: "#f5f5f5",
1305
+ marginBottom: 10,
1306
+ textAlign: "center"
1223
1307
  },
1224
- errorContainer: {
1225
- flex: 1,
1226
- justifyContent: "center",
1308
+ fallbackDescription: {
1309
+ fontSize: 14,
1310
+ color: "rgba(255,255,255,0.55)",
1311
+ textAlign: "center",
1312
+ marginBottom: 28,
1313
+ lineHeight: 20
1314
+ },
1315
+ retryButton: {
1316
+ width: "100%",
1317
+ backgroundColor: "#ffffff",
1318
+ borderRadius: 10,
1319
+ paddingVertical: 14,
1227
1320
  alignItems: "center",
1228
- padding: 20
1321
+ marginBottom: 12
1229
1322
  },
1230
- errorText: {
1231
- fontSize: 16,
1232
- color: "#ef4444",
1233
- textAlign: "center"
1234
- }
1323
+ retryButtonText: { fontSize: 15, fontWeight: "600", color: "#111010" },
1324
+ closeButton: {
1325
+ width: "100%",
1326
+ borderRadius: 10,
1327
+ paddingVertical: 14,
1328
+ alignItems: "center",
1329
+ backgroundColor: "rgba(255,255,255,0.1)"
1330
+ },
1331
+ closeButtonText: { fontSize: 15, fontWeight: "500", color: "rgba(255,255,255,0.7)" }
1235
1332
  });
1236
1333
 
1237
1334
  // src/domains/paywall/PaywallErrorBoundary.tsx
@@ -1546,6 +1643,92 @@ var PaywalloAffiliate = {
1546
1643
  getWithdrawals: () => affiliateManager.getWithdrawals()
1547
1644
  };
1548
1645
 
1646
+ // src/domains/subscription/SubscriptionCache.ts
1647
+ var CACHE_KEY = "subscription_cache";
1648
+ var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
1649
+ var SubscriptionCache = class {
1650
+ constructor(ttl = DEFAULT_TTL) {
1651
+ this.memoryCache = null;
1652
+ this.ttl = ttl;
1653
+ this.storage = new SecureStorage();
1654
+ }
1655
+ async get() {
1656
+ if (this.memoryCache && !this.isExpired(this.memoryCache)) {
1657
+ return this.memoryCache;
1658
+ }
1659
+ try {
1660
+ const stored = await this.storage.get(CACHE_KEY);
1661
+ if (!stored) {
1662
+ return null;
1663
+ }
1664
+ const parsed = JSON.parse(stored);
1665
+ if (typeof parsed !== "object" || parsed === null || !("data" in parsed) || typeof parsed.data !== "object" || parsed.data === null || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") {
1666
+ await this.storage.remove(CACHE_KEY);
1667
+ return null;
1668
+ }
1669
+ const cached = parsed;
1670
+ if (cached.data.subscription?.expiresAt) {
1671
+ cached.data.subscription.expiresAt = new Date(cached.data.subscription.expiresAt);
1672
+ }
1673
+ if (cached.data.subscription?.originalPurchaseDate) {
1674
+ cached.data.subscription.originalPurchaseDate = new Date(
1675
+ cached.data.subscription.originalPurchaseDate
1676
+ );
1677
+ }
1678
+ if (cached.data.subscription?.latestPurchaseDate) {
1679
+ cached.data.subscription.latestPurchaseDate = new Date(
1680
+ cached.data.subscription.latestPurchaseDate
1681
+ );
1682
+ }
1683
+ if (cached.data.subscription?.cancellationDate) {
1684
+ cached.data.subscription.cancellationDate = new Date(
1685
+ cached.data.subscription.cancellationDate
1686
+ );
1687
+ }
1688
+ if (cached.data.subscription?.gracePeriodExpiresAt) {
1689
+ cached.data.subscription.gracePeriodExpiresAt = new Date(
1690
+ cached.data.subscription.gracePeriodExpiresAt
1691
+ );
1692
+ }
1693
+ cached.isStale = this.isExpired(cached);
1694
+ if (!cached.isStale) {
1695
+ this.memoryCache = cached;
1696
+ }
1697
+ return cached;
1698
+ } catch {
1699
+ return null;
1700
+ }
1701
+ }
1702
+ async set(data) {
1703
+ const cached = {
1704
+ data,
1705
+ cachedAt: Date.now(),
1706
+ isStale: false
1707
+ };
1708
+ this.memoryCache = cached;
1709
+ try {
1710
+ await this.storage.set(CACHE_KEY, JSON.stringify(cached));
1711
+ } catch (error) {
1712
+ if (__DEV__) console.warn("[SubscriptionCache] set() failed:", error instanceof Error ? error.message : String(error));
1713
+ }
1714
+ }
1715
+ async invalidate() {
1716
+ this.memoryCache = null;
1717
+ try {
1718
+ await this.storage.remove(CACHE_KEY);
1719
+ } catch (error) {
1720
+ if (__DEV__) console.warn("[SubscriptionCache] invalidate() failed:", error instanceof Error ? error.message : String(error));
1721
+ }
1722
+ }
1723
+ isExpired(cached) {
1724
+ return Date.now() - cached.cachedAt > this.ttl;
1725
+ }
1726
+ setTTL(ttl) {
1727
+ this.ttl = ttl;
1728
+ }
1729
+ };
1730
+ var subscriptionCache = new SubscriptionCache();
1731
+
1549
1732
  // src/core/ApiClient.ts
1550
1733
  var import_react_native7 = require("react-native");
1551
1734
 
@@ -2408,7 +2591,7 @@ var ApiCache = class {
2408
2591
  // src/core/ApiClient.ts
2409
2592
  var SDK_VERSION = "1.0.0";
2410
2593
  var DEFAULT_BASE_URL = "https://panel.lucasqueiroga.shop";
2411
- var ApiClient = class {
2594
+ var _ApiClient = class _ApiClient {
2412
2595
  constructor(appKey, debug = false, environment = "Production", offlineQueueEnabled = true, timeout = 3e4) {
2413
2596
  this.cache = new ApiCache();
2414
2597
  this.appKey = appKey;
@@ -2498,6 +2681,11 @@ var ApiClient = class {
2498
2681
  const key = `${flagKey}:${distinctId}`;
2499
2682
  const hit = this.cache.getVariant(key);
2500
2683
  if (hit !== void 0) return hit;
2684
+ const persisted = await this.readFlagFromStorage(flagKey, distinctId);
2685
+ if (persisted !== null) {
2686
+ this.cache.setVariant(key, persisted);
2687
+ return persisted;
2688
+ }
2501
2689
  try {
2502
2690
  const res = await this.get(`/api/v1/flags/${flagKey}?distinctId=${encodeURIComponent(distinctId)}`);
2503
2691
  if (res.status === 404) {
@@ -2512,6 +2700,7 @@ var ApiClient = class {
2512
2700
  return { variant: null };
2513
2701
  }
2514
2702
  this.cache.setVariant(key, res.data);
2703
+ await this.writeFlagToStorage(flagKey, distinctId, res.data);
2515
2704
  return res.data;
2516
2705
  } catch (error) {
2517
2706
  this.log("Failed to get variant:", flagKey, error);
@@ -2593,6 +2782,35 @@ var ApiClient = class {
2593
2782
  this.log("Subscription status:", result.hasActiveSubscription);
2594
2783
  return result;
2595
2784
  }
2785
+ async evaluateFlags(keys, distinctId) {
2786
+ const query = keys.map(encodeURIComponent).join(",");
2787
+ const path = `/api/v1/flags/evaluate?keys=${query}`;
2788
+ try {
2789
+ const res = await this.httpClient.get(path, {
2790
+ headers: { "X-App-Key": this.appKey, "x-distinct-id": distinctId }
2791
+ });
2792
+ if (!res.ok) {
2793
+ this.log("Non-OK response for evaluateFlags:", res.status);
2794
+ return {};
2795
+ }
2796
+ const raw = res.data;
2797
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
2798
+ const record = raw;
2799
+ const result = {};
2800
+ for (const key of Object.keys(record)) {
2801
+ const value = record[key];
2802
+ const variant = typeof value === "boolean" ? String(value) : typeof value === "string" ? value : null;
2803
+ result[key] = variant;
2804
+ const flagVariant = { variant };
2805
+ this.cache.setVariant(`${key}:${distinctId}`, flagVariant);
2806
+ await this.writeFlagToStorage(key, distinctId, flagVariant);
2807
+ }
2808
+ return result;
2809
+ } catch (error) {
2810
+ this.log("Failed to evaluateFlags:", error);
2811
+ return {};
2812
+ }
2813
+ }
2596
2814
  async getConditionalFlag(flagKey, context) {
2597
2815
  try {
2598
2816
  const url = buildConditionalFlagUrl(this.baseUrl, flagKey, context);
@@ -2609,7 +2827,43 @@ var ApiClient = class {
2609
2827
  return { value: false, flagKey };
2610
2828
  }
2611
2829
  }
2612
- // ─── Private helpers ─────────────────────────────────────────────────────────
2830
+ // ─── Cache-only reads ────────────────────────────────────────────────────────
2831
+ /**
2832
+ * Reads a flag variant from cache layers only — no network call.
2833
+ * Checks in-memory cache first, then SecureStorage.
2834
+ * Returns null when both layers miss.
2835
+ */
2836
+ async getVariantFromCache(flagKey, distinctId) {
2837
+ const key = `${flagKey}:${distinctId}`;
2838
+ const hit = this.cache.getVariant(key);
2839
+ if (hit !== void 0) return hit;
2840
+ const persisted = await this.readFlagFromStorage(flagKey, distinctId);
2841
+ if (persisted !== null) {
2842
+ this.cache.setVariant(key, persisted);
2843
+ return persisted;
2844
+ }
2845
+ return null;
2846
+ }
2847
+ async readFlagFromStorage(flagKey, distinctId) {
2848
+ try {
2849
+ const raw = await secureStorage.get(`flag:${flagKey}:${distinctId}`);
2850
+ if (!raw) return null;
2851
+ const parsed = JSON.parse(raw);
2852
+ if (typeof parsed !== "object" || parsed === null || !("variant" in parsed) || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") return null;
2853
+ const { variant, payload, cachedAt } = parsed;
2854
+ if (Date.now() - cachedAt > _ApiClient.FLAG_MAX_AGE_MS) return null;
2855
+ return { variant, payload };
2856
+ } catch {
2857
+ return null;
2858
+ }
2859
+ }
2860
+ async writeFlagToStorage(flagKey, distinctId, flag) {
2861
+ try {
2862
+ const value = JSON.stringify({ variant: flag.variant, payload: flag.payload, cachedAt: Date.now() });
2863
+ await secureStorage.set(`flag:${flagKey}:${distinctId}`, value);
2864
+ } catch {
2865
+ }
2866
+ }
2613
2867
  /**
2614
2868
  * POST with offline queue fallback. Enqueues when offline or on network failure.
2615
2869
  * Throws only when offlineQueueEnabled is false and the request fails.
@@ -2636,6 +2890,9 @@ var ApiClient = class {
2636
2890
  if (this.debug) console.warn("[Paywallo API]", ...args);
2637
2891
  }
2638
2892
  };
2893
+ // ─── Private helpers ─────────────────────────────────────────────────────────
2894
+ _ApiClient.FLAG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
2895
+ var ApiClient = _ApiClient;
2639
2896
 
2640
2897
  // src/domains/campaign/CampaignError.ts
2641
2898
  var CampaignError = class extends PaywalloError {
@@ -2663,18 +2920,21 @@ var CampaignGateService = class {
2663
2920
  if (!distinctId) return null;
2664
2921
  return apiClient.getCampaign(placement, distinctId, context);
2665
2922
  }
2666
- async presentCampaign(apiClient, placement, paywallPresenter, campaignPresenter, context) {
2923
+ async presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
2667
2924
  const campaign = await this.getCampaign(apiClient, placement, context);
2668
2925
  if (!campaign) {
2669
2926
  return { presented: false, purchased: false, cancelled: false, restored: false, error: new CampaignError(CAMPAIGN_ERROR_CODES.NOT_FOUND, "No campaign found") };
2670
2927
  }
2928
+ if (!context?.forceShow && await hasActive()) {
2929
+ return { presented: false, purchased: false, cancelled: false, restored: false, skippedReason: "subscriber" };
2930
+ }
2671
2931
  if (campaignPresenter) return campaignPresenter(campaign);
2672
2932
  if (!paywallPresenter) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "PaywalloProvider not mounted. Wrap your app with <PaywalloProvider>");
2673
2933
  return { ...await paywallPresenter(campaign.paywall.placement), campaignId: campaign.campaignId, variantKey: campaign.variantKey };
2674
2934
  }
2675
2935
  async requireSubscriptionWithCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
2676
2936
  if (await hasActive()) return true;
2677
- const r = await this.presentCampaign(apiClient, placement, paywallPresenter, campaignPresenter, context);
2937
+ const r = await this.presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context);
2678
2938
  return r.purchased || r.restored;
2679
2939
  }
2680
2940
  async preloadCampaign(apiClient, placement, context) {
@@ -3211,6 +3471,20 @@ function isValidEventName(name) {
3211
3471
  if (name.startsWith("$")) return /^\$[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
3212
3472
  return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
3213
3473
  }
3474
+ var OFFLINE_ACTIVE_STATUSES = ["active", "grace_period"];
3475
+ async function resolveClientOfflineFromCache() {
3476
+ try {
3477
+ const cached = await subscriptionCache.get();
3478
+ if (!cached) return false;
3479
+ const sub = cached.data.subscription;
3480
+ if (!sub) return false;
3481
+ if (!OFFLINE_ACTIVE_STATUSES.includes(sub.status)) return false;
3482
+ if (!sub.expiresAt) return true;
3483
+ return sub.expiresAt > /* @__PURE__ */ new Date();
3484
+ } catch {
3485
+ return false;
3486
+ }
3487
+ }
3214
3488
  var _PaywalloClientClass = class _PaywalloClientClass {
3215
3489
  constructor() {
3216
3490
  this.config = null;
@@ -3227,27 +3501,27 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3227
3501
  this.initAttempts = 0;
3228
3502
  }
3229
3503
  async init(config) {
3230
- console.warn("[Paywallo INIT]", "init() called \u2014 appKey:", config.appKey ? config.appKey.substring(0, 8) + "..." : "MISSING");
3504
+ if (config.debug) console.warn("[Paywallo INIT]", "init() called \u2014 appKey:", config.appKey ? config.appKey.substring(0, 8) + "..." : "MISSING");
3231
3505
  if (this.config && this.apiClient) {
3232
- console.warn("[Paywallo INIT]", "Already initialized \u2014 skipping");
3506
+ if (config.debug) console.warn("[Paywallo INIT]", "Already initialized \u2014 skipping");
3233
3507
  return;
3234
3508
  }
3235
3509
  if (this.initPromise) {
3236
- console.warn("[Paywallo INIT]", "Init already in progress \u2014 awaiting...");
3510
+ if (config.debug) console.warn("[Paywallo INIT]", "Init already in progress \u2014 awaiting...");
3237
3511
  await this.initPromise;
3238
- console.warn("[Paywallo INIT]", "Awaited existing init \u2014 done");
3512
+ if (config.debug) console.warn("[Paywallo INIT]", "Awaited existing init \u2014 done");
3239
3513
  return;
3240
3514
  }
3241
3515
  if (!config.appKey) throw new ClientError(CLIENT_ERROR_CODES.MISSING_APP_KEY, "appKey is required");
3242
- console.warn("[Paywallo INIT]", "Starting fresh init...");
3516
+ if (config.debug) console.warn("[Paywallo INIT]", "Starting fresh init...");
3243
3517
  this.pendingConfig = config;
3244
3518
  this.initAttempts = 0;
3245
3519
  this.initPromise = this.initWithRetry(config);
3246
3520
  try {
3247
3521
  await this.initPromise;
3248
- console.warn("[Paywallo INIT]", "init() SUCCESS");
3522
+ if (config.debug) console.warn("[Paywallo INIT]", "init() SUCCESS");
3249
3523
  } catch (error) {
3250
- console.warn("[Paywallo INIT]", "init() FAILED:", error instanceof Error ? error.message : String(error));
3524
+ if (config.debug) console.warn("[Paywallo INIT]", "init() FAILED:", error instanceof Error ? error.message : String(error));
3251
3525
  this.initPromise = null;
3252
3526
  this.config = null;
3253
3527
  this.apiClient = null;
@@ -3317,26 +3591,26 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3317
3591
  async doInit(config) {
3318
3592
  const environment = config.environment ?? (typeof __DEV__ !== "undefined" && __DEV__ ? "Sandbox" : "Production");
3319
3593
  const offlineQueueEnabled = config.offlineQueueEnabled !== false;
3320
- console.warn("[Paywallo INIT]", "Step 1: networkMonitor + offlineQueue...");
3594
+ if (config.debug) console.warn("[Paywallo INIT]", "Step 1: networkMonitor + offlineQueue...");
3321
3595
  await Promise.all([
3322
3596
  networkMonitor.initialize({ debug: config.debug }),
3323
3597
  offlineQueue.initialize({ debug: config.debug }).then(
3324
3598
  () => offlineQueue.clearItemsWithInvalidAppKey(config.appKey)
3325
3599
  )
3326
3600
  ]);
3327
- console.warn("[Paywallo INIT]", "Step 1 DONE");
3328
- console.warn("[Paywallo INIT]", "Step 2: ApiClient + queueProcessor + affiliate...");
3601
+ if (config.debug) console.warn("[Paywallo INIT]", "Step 1 DONE");
3602
+ if (config.debug) console.warn("[Paywallo INIT]", "Step 2: ApiClient + queueProcessor + affiliate...");
3329
3603
  this.apiClient = new ApiClient(config.appKey, config.debug, environment, offlineQueueEnabled, config.timeout);
3330
3604
  queueProcessor.initialize(this.apiClient.getHttpClient(), { debug: config.debug });
3331
3605
  affiliateManager.initialize(this.apiClient, config.debug);
3332
- console.warn("[Paywallo INIT]", "Step 2 DONE");
3333
- console.warn("[Paywallo INIT]", "Step 3: identityManager + sessionManager...");
3606
+ if (config.debug) console.warn("[Paywallo INIT]", "Step 2 DONE");
3607
+ if (config.debug) console.warn("[Paywallo INIT]", "Step 3: identityManager + sessionManager...");
3334
3608
  await Promise.all([
3335
3609
  identityManager.initialize(this.apiClient, config.debug),
3336
3610
  sessionManager.initialize(this.apiClient, config.sessionConfig, config.debug)
3337
3611
  ]);
3338
3612
  const distinctId = identityManager.getDistinctId();
3339
- console.warn("[Paywallo INIT]", "Step 3 DONE \u2014 distinctId:", distinctId ? distinctId.substring(0, 8) + "..." : "EMPTY");
3613
+ if (config.debug) console.warn("[Paywallo INIT]", "Step 3 DONE \u2014 distinctId:", distinctId ? distinctId.substring(0, 8) + "..." : "EMPTY");
3340
3614
  if (config.autoStartSession !== false) {
3341
3615
  sessionManager.startSession().catch(() => {
3342
3616
  if (config.debug) console.warn("[Paywallo]", "Session start failed during init \u2014 continuing");
@@ -3345,7 +3619,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3345
3619
  new InstallTracker(config.debug).trackIfNeeded(this.apiClient, config.requestATT === true).catch(() => {
3346
3620
  });
3347
3621
  this.config = config;
3348
- console.warn("[Paywallo INIT]", "COMPLETE \u2014 SDK ready, environment:", environment);
3622
+ if (config.debug) console.warn("[Paywallo INIT]", "COMPLETE \u2014 SDK ready, environment:", environment);
3349
3623
  }
3350
3624
  async identify(options) {
3351
3625
  this.ensureInitialized();
@@ -3355,7 +3629,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3355
3629
  const apiClient = this.getApiClientOrThrow();
3356
3630
  const distinctId = identityManager.getDistinctId();
3357
3631
  if (!distinctId) {
3358
- console.warn("[Paywallo TRACK]", "SKIPPED \u2014 no distinctId", eventName);
3632
+ if (this.config?.debug) console.warn("[Paywallo TRACK]", "SKIPPED \u2014 no distinctId", eventName);
3359
3633
  return;
3360
3634
  }
3361
3635
  if (!isValidEventName(eventName)) {
@@ -3364,7 +3638,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3364
3638
  }
3365
3639
  const sessionId = sessionManager.getSessionId();
3366
3640
  if (eventName === "$purchase_completed") {
3367
- console.warn("[Paywallo PURCHASE]", "tracking purchase:", JSON.stringify({
3641
+ if (this.config?.debug) console.warn("[Paywallo PURCHASE]", "tracking purchase:", JSON.stringify({
3368
3642
  distinctId: distinctId.substring(0, 8) + "...",
3369
3643
  productId: options?.properties?.productId,
3370
3644
  priceUsd: options?.properties?.priceUsd,
@@ -3373,14 +3647,14 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3373
3647
  placement: options?.properties?.placement
3374
3648
  }));
3375
3649
  } else if (eventName === "$paywall_viewed" || eventName === "$paywall_closed") {
3376
- console.warn("[Paywallo PAYWALL]", eventName, JSON.stringify({
3650
+ if (this.config?.debug) console.warn("[Paywallo PAYWALL]", eventName, JSON.stringify({
3377
3651
  distinctId: distinctId.substring(0, 8) + "...",
3378
3652
  placement: options?.properties?.placement,
3379
3653
  variantKey: options?.properties?.variantKey,
3380
3654
  timeOnPaywall: options?.properties?.timeOnPaywall
3381
3655
  }));
3382
3656
  } else {
3383
- console.warn("[Paywallo TRACK]", eventName, "distinctId:", distinctId.substring(0, 8) + "...");
3657
+ if (this.config?.debug) console.warn("[Paywallo TRACK]", eventName, "distinctId:", distinctId.substring(0, 8) + "...");
3384
3658
  }
3385
3659
  await apiClient.trackEvent(eventName, distinctId, {
3386
3660
  ...identityManager.getProperties(),
@@ -3431,15 +3705,25 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3431
3705
  ...sessionId && { sessionId }
3432
3706
  });
3433
3707
  }
3708
+ async getVariantCached(flagKey, defaultValue) {
3709
+ this.ensureInitialized();
3710
+ const apiClient = this.getApiClientOrThrow();
3711
+ const distinctId = identityManager.getDistinctId();
3712
+ const result = await apiClient.getVariantFromCache(flagKey, distinctId);
3713
+ if (result) return result;
3714
+ void apiClient.getVariant(flagKey, distinctId).catch(() => {
3715
+ });
3716
+ return { variant: defaultValue ?? null, payload: void 0 };
3717
+ }
3434
3718
  async getVariant(flagKey) {
3435
3719
  try {
3436
3720
  const distinctId = identityManager.getDistinctId();
3437
3721
  if (!distinctId) {
3438
- console.warn("[Paywallo AB]", "getVariant SKIPPED \u2014 no distinctId", flagKey);
3722
+ if (this.config?.debug) console.warn("[Paywallo AB]", "getVariant SKIPPED \u2014 no distinctId", flagKey);
3439
3723
  return { variant: null };
3440
3724
  }
3441
3725
  const result = await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
3442
- console.warn("[Paywallo AB]", `getVariant("${flagKey}") =`, result.variant);
3726
+ if (this.config?.debug) console.warn("[Paywallo AB]", `getVariant("${flagKey}") =`, result.variant);
3443
3727
  return result;
3444
3728
  } catch (error) {
3445
3729
  if (this.config?.debug) console.warn("[Paywallo]", "getVariant failed \u2014 returning null", error);
@@ -3447,6 +3731,10 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3447
3731
  return { variant: null };
3448
3732
  }
3449
3733
  }
3734
+ async evaluateFlags(keys) {
3735
+ this.ensureInitialized();
3736
+ return this.getApiClientOrThrow().evaluateFlags(keys, identityManager.getDistinctId());
3737
+ }
3450
3738
  async getConditionalFlag(flagKey, context) {
3451
3739
  try {
3452
3740
  const distinctId = identityManager.getDistinctId();
@@ -3472,7 +3760,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3472
3760
  }
3473
3761
  async presentCampaign(placement, context) {
3474
3762
  this.ensureInitialized();
3475
- return campaignGateService.presentCampaign(this.getApiClientOrThrow(), placement, this.paywallPresenter, this.campaignPresenter, context);
3763
+ return campaignGateService.presentCampaign(this.getApiClientOrThrow(), placement, () => this.hasActiveSubscription(), this.paywallPresenter, this.campaignPresenter, context);
3476
3764
  }
3477
3765
  async hasActiveSubscription() {
3478
3766
  this.ensureInitialized();
@@ -3480,11 +3768,12 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3480
3768
  if (this.activeChecker) return await this.activeChecker();
3481
3769
  const distinctId = identityManager.getDistinctId();
3482
3770
  if (!distinctId || !this.apiClient) return false;
3483
- return (await this.apiClient.getSubscriptionStatus(distinctId)).hasActiveSubscription;
3771
+ const status = await this.apiClient.getSubscriptionStatus(distinctId);
3772
+ return status.hasActiveSubscription;
3484
3773
  } catch {
3485
- if (this.config?.debug) console.warn("[Paywallo]", "hasActiveSubscription failed \u2014 returning false");
3774
+ if (this.config?.debug) console.warn("[Paywallo]", "hasActiveSubscription failed \u2014 using persistent cache");
3486
3775
  if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", "hasActiveSubscription failed");
3487
- return false;
3776
+ return resolveClientOfflineFromCache();
3488
3777
  }
3489
3778
  }
3490
3779
  async getSubscription() {
@@ -3513,6 +3802,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3513
3802
  const wasDebug = this.config?.debug ?? false;
3514
3803
  await sessionManager.endSession();
3515
3804
  await identityManager.reset();
3805
+ await subscriptionCache.invalidate();
3516
3806
  queueProcessor.dispose();
3517
3807
  offlineQueue.dispose();
3518
3808
  networkMonitor.dispose();
@@ -3869,86 +4159,6 @@ var import_react13 = require("react");
3869
4159
 
3870
4160
  // src/domains/subscription/SubscriptionManager.ts
3871
4161
  var import_react_native13 = require("react-native");
3872
-
3873
- // src/domains/subscription/SubscriptionCache.ts
3874
- var CACHE_KEY = "subscription_cache";
3875
- var DEFAULT_TTL = 5 * 60 * 1e3;
3876
- var SubscriptionCache = class {
3877
- constructor(ttl = DEFAULT_TTL) {
3878
- this.memoryCache = null;
3879
- this.ttl = ttl;
3880
- this.storage = new SecureStorage();
3881
- }
3882
- async get() {
3883
- if (this.memoryCache && !this.isExpired(this.memoryCache)) {
3884
- return this.memoryCache;
3885
- }
3886
- try {
3887
- const stored = await this.storage.get(CACHE_KEY);
3888
- if (!stored) {
3889
- return null;
3890
- }
3891
- const cached = JSON.parse(stored);
3892
- if (cached.data.subscription?.expiresAt) {
3893
- cached.data.subscription.expiresAt = new Date(cached.data.subscription.expiresAt);
3894
- }
3895
- if (cached.data.subscription?.originalPurchaseDate) {
3896
- cached.data.subscription.originalPurchaseDate = new Date(
3897
- cached.data.subscription.originalPurchaseDate
3898
- );
3899
- }
3900
- if (cached.data.subscription?.latestPurchaseDate) {
3901
- cached.data.subscription.latestPurchaseDate = new Date(
3902
- cached.data.subscription.latestPurchaseDate
3903
- );
3904
- }
3905
- if (cached.data.subscription?.cancellationDate) {
3906
- cached.data.subscription.cancellationDate = new Date(
3907
- cached.data.subscription.cancellationDate
3908
- );
3909
- }
3910
- if (cached.data.subscription?.gracePeriodExpiresAt) {
3911
- cached.data.subscription.gracePeriodExpiresAt = new Date(
3912
- cached.data.subscription.gracePeriodExpiresAt
3913
- );
3914
- }
3915
- cached.isStale = this.isExpired(cached);
3916
- if (!cached.isStale) {
3917
- this.memoryCache = cached;
3918
- }
3919
- return cached;
3920
- } catch {
3921
- return null;
3922
- }
3923
- }
3924
- async set(data) {
3925
- const cached = {
3926
- data,
3927
- cachedAt: Date.now(),
3928
- isStale: false
3929
- };
3930
- this.memoryCache = cached;
3931
- try {
3932
- await this.storage.set(CACHE_KEY, JSON.stringify(cached));
3933
- } catch {
3934
- }
3935
- }
3936
- async invalidate() {
3937
- this.memoryCache = null;
3938
- try {
3939
- await this.storage.remove(CACHE_KEY);
3940
- } catch {
3941
- }
3942
- }
3943
- isExpired(cached) {
3944
- return Date.now() - cached.cachedAt > this.ttl;
3945
- }
3946
- setTTL(ttl) {
3947
- this.ttl = ttl;
3948
- }
3949
- };
3950
-
3951
- // src/domains/subscription/SubscriptionManager.ts
3952
4162
  var SubscriptionManagerClass = class {
3953
4163
  constructor() {
3954
4164
  this.config = null;
@@ -4364,12 +4574,54 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4364
4574
  // src/hooks/useSubscriptionSync.ts
4365
4575
  var import_react17 = require("react");
4366
4576
  var CACHE_TTL_MS2 = 5 * 60 * 1e3;
4367
- var ACTIVE_STATUSES = ["active", "in_grace_period"];
4577
+ var ACTIVE_STATUSES = ["active", "grace_period"];
4578
+ function toDomainSubscriptionInfo(sub) {
4579
+ return {
4580
+ id: "",
4581
+ productId: sub.productId,
4582
+ // The hook's SubscriptionStatus is a subset of the domain's — cast is safe
4583
+ status: sub.status,
4584
+ expiresAt: sub.expiresAt ?? /* @__PURE__ */ new Date(0),
4585
+ originalPurchaseDate: /* @__PURE__ */ new Date(0),
4586
+ latestPurchaseDate: /* @__PURE__ */ new Date(0),
4587
+ willRenew: sub.autoRenewEnabled,
4588
+ isTrial: false,
4589
+ isIntroductoryPeriod: false,
4590
+ platform: sub.platform
4591
+ };
4592
+ }
4593
+ function toDomainResponse(status) {
4594
+ return {
4595
+ hasActiveSubscription: status.hasActiveSubscription,
4596
+ subscription: status.subscription ? toDomainSubscriptionInfo(status.subscription) : null,
4597
+ entitlements: []
4598
+ };
4599
+ }
4600
+ async function resolveOfflineFromCache() {
4601
+ try {
4602
+ const cached = await subscriptionCache.get();
4603
+ if (!cached) return false;
4604
+ const sub = cached.data.subscription;
4605
+ if (!sub) return false;
4606
+ if (!ACTIVE_STATUSES.includes(sub.status)) return false;
4607
+ const hasValidExpiry = sub.expiresAt.getTime() === 0 || sub.expiresAt > /* @__PURE__ */ new Date();
4608
+ return hasValidExpiry;
4609
+ } catch {
4610
+ return false;
4611
+ }
4612
+ }
4613
+ function isActiveCacheEntry(entry) {
4614
+ if (!entry.data) return false;
4615
+ if (!ACTIVE_STATUSES.includes(entry.data.status)) return false;
4616
+ if (entry.data.expiresAt && entry.data.expiresAt < /* @__PURE__ */ new Date()) return false;
4617
+ return true;
4618
+ }
4368
4619
  function useSubscriptionSync() {
4369
4620
  const [subscription, setSubscription] = (0, import_react17.useState)(null);
4370
4621
  const cacheRef = (0, import_react17.useRef)(null);
4371
4622
  const invalidateCache = (0, import_react17.useCallback)(() => {
4372
4623
  cacheRef.current = null;
4624
+ void subscriptionCache.invalidate();
4373
4625
  }, []);
4374
4626
  const fetchAndCache = (0, import_react17.useCallback)(async () => {
4375
4627
  const apiClient = PaywalloClient.getApiClient();
@@ -4382,6 +4634,7 @@ function useSubscriptionSync() {
4382
4634
  } : null;
4383
4635
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4384
4636
  setSubscription(sub);
4637
+ void subscriptionCache.set(toDomainResponse(status));
4385
4638
  return sub;
4386
4639
  }, []);
4387
4640
  const hasActiveSubscription = (0, import_react17.useCallback)(async () => {
@@ -4390,10 +4643,28 @@ function useSubscriptionSync() {
4390
4643
  if (!apiClient || !distinctId) return false;
4391
4644
  const cached = cacheRef.current;
4392
4645
  if (cached && cached.expiresAt > Date.now()) {
4393
- if (!cached.data) return false;
4394
- if (!ACTIVE_STATUSES.includes(cached.data.status)) return false;
4395
- if (cached.data.expiresAt && cached.data.expiresAt < /* @__PURE__ */ new Date()) return false;
4396
- return true;
4646
+ return isActiveCacheEntry(cached);
4647
+ }
4648
+ try {
4649
+ const persisted = await subscriptionCache.get();
4650
+ if (persisted) {
4651
+ if (!persisted.isStale) {
4652
+ const sub = persisted.data.subscription ? {
4653
+ productId: persisted.data.subscription.productId,
4654
+ status: persisted.data.subscription.status,
4655
+ expiresAt: persisted.data.subscription.expiresAt ?? null,
4656
+ platform: persisted.data.subscription.platform,
4657
+ autoRenewEnabled: persisted.data.subscription.willRenew,
4658
+ inGracePeriod: persisted.data.subscription.status === "grace_period"
4659
+ } : null;
4660
+ cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4661
+ setSubscription(sub);
4662
+ return persisted.data.hasActiveSubscription;
4663
+ }
4664
+ void fetchAndCache();
4665
+ return persisted.data.hasActiveSubscription;
4666
+ }
4667
+ } catch {
4397
4668
  }
4398
4669
  try {
4399
4670
  const status = await apiClient.getSubscriptionStatus(distinctId);
@@ -4403,11 +4674,12 @@ function useSubscriptionSync() {
4403
4674
  } : null;
4404
4675
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4405
4676
  setSubscription(sub);
4677
+ void subscriptionCache.set(toDomainResponse(status));
4406
4678
  return status.hasActiveSubscription;
4407
4679
  } catch {
4408
- return false;
4680
+ return await resolveOfflineFromCache();
4409
4681
  }
4410
- }, []);
4682
+ }, [fetchAndCache]);
4411
4683
  const getSubscription = (0, import_react17.useCallback)(async () => {
4412
4684
  const apiClient = PaywalloClient.getApiClient();
4413
4685
  const distinctId = PaywalloClient.getDistinctId();
@@ -4417,7 +4689,22 @@ function useSubscriptionSync() {
4417
4689
  try {
4418
4690
  return await fetchAndCache();
4419
4691
  } catch {
4420
- return null;
4692
+ try {
4693
+ const persisted = await subscriptionCache.get();
4694
+ if (!persisted?.data.subscription) return null;
4695
+ const s = persisted.data.subscription;
4696
+ const normalisedStatus = s.status === "grace_period" ? "in_grace_period" : s.status;
4697
+ return {
4698
+ productId: s.productId,
4699
+ status: normalisedStatus,
4700
+ expiresAt: s.expiresAt ?? null,
4701
+ platform: s.platform,
4702
+ autoRenewEnabled: s.willRenew,
4703
+ inGracePeriod: normalisedStatus === "in_grace_period"
4704
+ };
4705
+ } catch {
4706
+ return null;
4707
+ }
4421
4708
  }
4422
4709
  }, [fetchAndCache]);
4423
4710
  return { subscription, hasActiveSubscription, getSubscription, invalidateCache };