@virex-tech/paywallo-sdk 1.1.1 → 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
@@ -951,11 +951,6 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
951
951
  const storeProducts = await getIAPService().loadProducts(productIds);
952
952
  const map = /* @__PURE__ */ new Map();
953
953
  for (const p of storeProducts) map.set(p.productId, p);
954
- if (PaywalloClient.getConfig()?.debug) {
955
- for (const [id, p] of map) {
956
- console.warn(`[Paywallo][CURRENCY-CHECK] product=${id} currency=${p.currency} price="${p.price}" localizedPrice="${p.localizedPrice}" priceValue=${p.priceValue} fallbackUSD=${!p.currency || p.currency === "USD" ? "POSSIBLE" : "NO"}`);
957
- }
958
- }
959
954
  setProducts(map);
960
955
  } catch {
961
956
  setProducts(/* @__PURE__ */ new Map());
@@ -1035,6 +1030,13 @@ function getWebViewEmitter() {
1035
1030
  }
1036
1031
  return webViewEmitter;
1037
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
+ }
1038
1040
  function PaywallWebView({
1039
1041
  paywallId,
1040
1042
  craftData,
@@ -1045,11 +1047,14 @@ function PaywallWebView({
1045
1047
  onPurchase,
1046
1048
  onClose,
1047
1049
  onRestore,
1048
- onReady
1050
+ onReady,
1051
+ onError
1049
1052
  }) {
1050
1053
  const paywallDataSent = (0, import_react4.useRef)(false);
1051
1054
  const loadEndReceived = (0, import_react4.useRef)(false);
1052
1055
  const [scriptToInject, setScriptToInject] = (0, import_react4.useState)(void 0);
1056
+ const onErrorRef = (0, import_react4.useRef)(onError);
1057
+ onErrorRef.current = onError;
1053
1058
  const NativeWebView = (0, import_react4.useMemo)(() => getNativeWebView(), []);
1054
1059
  const webUrl = (0, import_react4.useMemo)(() => PaywalloClient.getWebUrl() ?? "http://localhost:3000", []);
1055
1060
  const buildAndSetInjectionScript = (0, import_react4.useCallback)(() => {
@@ -1109,17 +1114,51 @@ function PaywallWebView({
1109
1114
  const message = parseWebViewMessage(event.data);
1110
1115
  if (message) handleParsedMessage(message);
1111
1116
  });
1117
+ const errorSub = emitter.addListener(
1118
+ "PaywalloWebViewError",
1119
+ (event) => {
1120
+ onErrorRef.current?.(parseErrorEvent(event));
1121
+ }
1122
+ );
1112
1123
  return () => {
1113
1124
  loadEndSub.remove();
1114
1125
  messageSub.remove();
1126
+ errorSub.remove();
1115
1127
  };
1116
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
+ }, []);
1117
1140
  const handleLoadEnd = (0, import_react4.useCallback)(() => {
1118
1141
  if (!loadEndReceived.current) {
1119
1142
  loadEndReceived.current = true;
1120
1143
  buildAndSetInjectionScript();
1121
1144
  }
1122
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
+ }, []);
1123
1162
  const handleMessage = (0, import_react4.useCallback)(
1124
1163
  (event) => {
1125
1164
  const message = parseWebViewMessage(event.nativeEvent.data);
@@ -1139,6 +1178,7 @@ function PaywallWebView({
1139
1178
  injectedJavaScript: scriptToInject,
1140
1179
  onMessage: handleMessage,
1141
1180
  onLoadEnd: handleLoadEnd,
1181
+ onError: handleNativeError,
1142
1182
  style: styles.webview
1143
1183
  }
1144
1184
  ) });
@@ -1169,6 +1209,25 @@ function PaywallModal({
1169
1209
  campaignId
1170
1210
  );
1171
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
+ }, []);
1172
1231
  const openAnim = (0, import_react5.useRef)(new import_react_native5.Animated.Value(SCREEN_HEIGHT2)).current;
1173
1232
  (0, import_react5.useEffect)(() => {
1174
1233
  if (visible) {
@@ -1193,7 +1252,12 @@ function PaywallModal({
1193
1252
  statusBarTranslucent: true,
1194
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: [
1195
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 }) }),
1196
- !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)(
1197
1261
  PaywallWebView,
1198
1262
  {
1199
1263
  paywallId: paywallConfig.id,
@@ -1204,39 +1268,67 @@ function PaywallModal({
1204
1268
  isPurchasing,
1205
1269
  onClose: handleClose,
1206
1270
  onPurchase: handlePurchase,
1207
- onRestore: handleRestore
1208
- }
1209
- )
1271
+ onRestore: handleRestore,
1272
+ onError: handleWebViewError
1273
+ },
1274
+ retryCount
1275
+ ) })
1210
1276
  ] }) }) })
1211
1277
  }
1212
1278
  );
1213
1279
  }
1214
1280
  var styles2 = import_react_native5.StyleSheet.create({
1215
- 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: {
1216
1287
  flex: 1,
1217
- backgroundColor: "rgba(0,0,0,0.3)"
1288
+ justifyContent: "center",
1289
+ alignItems: "center",
1290
+ backgroundColor: "#111010",
1291
+ padding: 24
1218
1292
  },
1219
- animatedContainer: {
1220
- flex: 1,
1221
- backgroundColor: "#fff",
1222
- borderTopLeftRadius: 0,
1223
- 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
1224
1300
  },
1225
- container: {
1226
- flex: 1,
1227
- backgroundColor: "#fff"
1301
+ fallbackTitle: {
1302
+ fontSize: 18,
1303
+ fontWeight: "600",
1304
+ color: "#f5f5f5",
1305
+ marginBottom: 10,
1306
+ textAlign: "center"
1228
1307
  },
1229
- errorContainer: {
1230
- flex: 1,
1231
- 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,
1232
1320
  alignItems: "center",
1233
- padding: 20
1321
+ marginBottom: 12
1234
1322
  },
1235
- errorText: {
1236
- fontSize: 16,
1237
- color: "#ef4444",
1238
- textAlign: "center"
1239
- }
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)" }
1240
1332
  });
1241
1333
 
1242
1334
  // src/domains/paywall/PaywallErrorBoundary.tsx
@@ -1551,6 +1643,92 @@ var PaywalloAffiliate = {
1551
1643
  getWithdrawals: () => affiliateManager.getWithdrawals()
1552
1644
  };
1553
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
+
1554
1732
  // src/core/ApiClient.ts
1555
1733
  var import_react_native7 = require("react-native");
1556
1734
 
@@ -2413,7 +2591,7 @@ var ApiCache = class {
2413
2591
  // src/core/ApiClient.ts
2414
2592
  var SDK_VERSION = "1.0.0";
2415
2593
  var DEFAULT_BASE_URL = "https://panel.lucasqueiroga.shop";
2416
- var ApiClient = class {
2594
+ var _ApiClient = class _ApiClient {
2417
2595
  constructor(appKey, debug = false, environment = "Production", offlineQueueEnabled = true, timeout = 3e4) {
2418
2596
  this.cache = new ApiCache();
2419
2597
  this.appKey = appKey;
@@ -2503,6 +2681,11 @@ var ApiClient = class {
2503
2681
  const key = `${flagKey}:${distinctId}`;
2504
2682
  const hit = this.cache.getVariant(key);
2505
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
+ }
2506
2689
  try {
2507
2690
  const res = await this.get(`/api/v1/flags/${flagKey}?distinctId=${encodeURIComponent(distinctId)}`);
2508
2691
  if (res.status === 404) {
@@ -2517,6 +2700,7 @@ var ApiClient = class {
2517
2700
  return { variant: null };
2518
2701
  }
2519
2702
  this.cache.setVariant(key, res.data);
2703
+ await this.writeFlagToStorage(flagKey, distinctId, res.data);
2520
2704
  return res.data;
2521
2705
  } catch (error) {
2522
2706
  this.log("Failed to get variant:", flagKey, error);
@@ -2598,6 +2782,35 @@ var ApiClient = class {
2598
2782
  this.log("Subscription status:", result.hasActiveSubscription);
2599
2783
  return result;
2600
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
+ }
2601
2814
  async getConditionalFlag(flagKey, context) {
2602
2815
  try {
2603
2816
  const url = buildConditionalFlagUrl(this.baseUrl, flagKey, context);
@@ -2614,7 +2827,43 @@ var ApiClient = class {
2614
2827
  return { value: false, flagKey };
2615
2828
  }
2616
2829
  }
2617
- // ─── 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
+ }
2618
2867
  /**
2619
2868
  * POST with offline queue fallback. Enqueues when offline or on network failure.
2620
2869
  * Throws only when offlineQueueEnabled is false and the request fails.
@@ -2641,6 +2890,9 @@ var ApiClient = class {
2641
2890
  if (this.debug) console.warn("[Paywallo API]", ...args);
2642
2891
  }
2643
2892
  };
2893
+ // ─── Private helpers ─────────────────────────────────────────────────────────
2894
+ _ApiClient.FLAG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
2895
+ var ApiClient = _ApiClient;
2644
2896
 
2645
2897
  // src/domains/campaign/CampaignError.ts
2646
2898
  var CampaignError = class extends PaywalloError {
@@ -2668,18 +2920,21 @@ var CampaignGateService = class {
2668
2920
  if (!distinctId) return null;
2669
2921
  return apiClient.getCampaign(placement, distinctId, context);
2670
2922
  }
2671
- async presentCampaign(apiClient, placement, paywallPresenter, campaignPresenter, context) {
2923
+ async presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
2672
2924
  const campaign = await this.getCampaign(apiClient, placement, context);
2673
2925
  if (!campaign) {
2674
2926
  return { presented: false, purchased: false, cancelled: false, restored: false, error: new CampaignError(CAMPAIGN_ERROR_CODES.NOT_FOUND, "No campaign found") };
2675
2927
  }
2928
+ if (!context?.forceShow && await hasActive()) {
2929
+ return { presented: false, purchased: false, cancelled: false, restored: false, skippedReason: "subscriber" };
2930
+ }
2676
2931
  if (campaignPresenter) return campaignPresenter(campaign);
2677
2932
  if (!paywallPresenter) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "PaywalloProvider not mounted. Wrap your app with <PaywalloProvider>");
2678
2933
  return { ...await paywallPresenter(campaign.paywall.placement), campaignId: campaign.campaignId, variantKey: campaign.variantKey };
2679
2934
  }
2680
2935
  async requireSubscriptionWithCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
2681
2936
  if (await hasActive()) return true;
2682
- const r = await this.presentCampaign(apiClient, placement, paywallPresenter, campaignPresenter, context);
2937
+ const r = await this.presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context);
2683
2938
  return r.purchased || r.restored;
2684
2939
  }
2685
2940
  async preloadCampaign(apiClient, placement, context) {
@@ -3216,6 +3471,20 @@ function isValidEventName(name) {
3216
3471
  if (name.startsWith("$")) return /^\$[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
3217
3472
  return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
3218
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
+ }
3219
3488
  var _PaywalloClientClass = class _PaywalloClientClass {
3220
3489
  constructor() {
3221
3490
  this.config = null;
@@ -3232,27 +3501,27 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3232
3501
  this.initAttempts = 0;
3233
3502
  }
3234
3503
  async init(config) {
3235
- 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");
3236
3505
  if (this.config && this.apiClient) {
3237
- console.warn("[Paywallo INIT]", "Already initialized \u2014 skipping");
3506
+ if (config.debug) console.warn("[Paywallo INIT]", "Already initialized \u2014 skipping");
3238
3507
  return;
3239
3508
  }
3240
3509
  if (this.initPromise) {
3241
- console.warn("[Paywallo INIT]", "Init already in progress \u2014 awaiting...");
3510
+ if (config.debug) console.warn("[Paywallo INIT]", "Init already in progress \u2014 awaiting...");
3242
3511
  await this.initPromise;
3243
- console.warn("[Paywallo INIT]", "Awaited existing init \u2014 done");
3512
+ if (config.debug) console.warn("[Paywallo INIT]", "Awaited existing init \u2014 done");
3244
3513
  return;
3245
3514
  }
3246
3515
  if (!config.appKey) throw new ClientError(CLIENT_ERROR_CODES.MISSING_APP_KEY, "appKey is required");
3247
- console.warn("[Paywallo INIT]", "Starting fresh init...");
3516
+ if (config.debug) console.warn("[Paywallo INIT]", "Starting fresh init...");
3248
3517
  this.pendingConfig = config;
3249
3518
  this.initAttempts = 0;
3250
3519
  this.initPromise = this.initWithRetry(config);
3251
3520
  try {
3252
3521
  await this.initPromise;
3253
- console.warn("[Paywallo INIT]", "init() SUCCESS");
3522
+ if (config.debug) console.warn("[Paywallo INIT]", "init() SUCCESS");
3254
3523
  } catch (error) {
3255
- 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));
3256
3525
  this.initPromise = null;
3257
3526
  this.config = null;
3258
3527
  this.apiClient = null;
@@ -3322,26 +3591,26 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3322
3591
  async doInit(config) {
3323
3592
  const environment = config.environment ?? (typeof __DEV__ !== "undefined" && __DEV__ ? "Sandbox" : "Production");
3324
3593
  const offlineQueueEnabled = config.offlineQueueEnabled !== false;
3325
- console.warn("[Paywallo INIT]", "Step 1: networkMonitor + offlineQueue...");
3594
+ if (config.debug) console.warn("[Paywallo INIT]", "Step 1: networkMonitor + offlineQueue...");
3326
3595
  await Promise.all([
3327
3596
  networkMonitor.initialize({ debug: config.debug }),
3328
3597
  offlineQueue.initialize({ debug: config.debug }).then(
3329
3598
  () => offlineQueue.clearItemsWithInvalidAppKey(config.appKey)
3330
3599
  )
3331
3600
  ]);
3332
- console.warn("[Paywallo INIT]", "Step 1 DONE");
3333
- 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...");
3334
3603
  this.apiClient = new ApiClient(config.appKey, config.debug, environment, offlineQueueEnabled, config.timeout);
3335
3604
  queueProcessor.initialize(this.apiClient.getHttpClient(), { debug: config.debug });
3336
3605
  affiliateManager.initialize(this.apiClient, config.debug);
3337
- console.warn("[Paywallo INIT]", "Step 2 DONE");
3338
- 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...");
3339
3608
  await Promise.all([
3340
3609
  identityManager.initialize(this.apiClient, config.debug),
3341
3610
  sessionManager.initialize(this.apiClient, config.sessionConfig, config.debug)
3342
3611
  ]);
3343
3612
  const distinctId = identityManager.getDistinctId();
3344
- 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");
3345
3614
  if (config.autoStartSession !== false) {
3346
3615
  sessionManager.startSession().catch(() => {
3347
3616
  if (config.debug) console.warn("[Paywallo]", "Session start failed during init \u2014 continuing");
@@ -3350,7 +3619,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3350
3619
  new InstallTracker(config.debug).trackIfNeeded(this.apiClient, config.requestATT === true).catch(() => {
3351
3620
  });
3352
3621
  this.config = config;
3353
- console.warn("[Paywallo INIT]", "COMPLETE \u2014 SDK ready, environment:", environment);
3622
+ if (config.debug) console.warn("[Paywallo INIT]", "COMPLETE \u2014 SDK ready, environment:", environment);
3354
3623
  }
3355
3624
  async identify(options) {
3356
3625
  this.ensureInitialized();
@@ -3360,7 +3629,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3360
3629
  const apiClient = this.getApiClientOrThrow();
3361
3630
  const distinctId = identityManager.getDistinctId();
3362
3631
  if (!distinctId) {
3363
- console.warn("[Paywallo TRACK]", "SKIPPED \u2014 no distinctId", eventName);
3632
+ if (this.config?.debug) console.warn("[Paywallo TRACK]", "SKIPPED \u2014 no distinctId", eventName);
3364
3633
  return;
3365
3634
  }
3366
3635
  if (!isValidEventName(eventName)) {
@@ -3369,7 +3638,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3369
3638
  }
3370
3639
  const sessionId = sessionManager.getSessionId();
3371
3640
  if (eventName === "$purchase_completed") {
3372
- console.warn("[Paywallo PURCHASE]", "tracking purchase:", JSON.stringify({
3641
+ if (this.config?.debug) console.warn("[Paywallo PURCHASE]", "tracking purchase:", JSON.stringify({
3373
3642
  distinctId: distinctId.substring(0, 8) + "...",
3374
3643
  productId: options?.properties?.productId,
3375
3644
  priceUsd: options?.properties?.priceUsd,
@@ -3378,14 +3647,14 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3378
3647
  placement: options?.properties?.placement
3379
3648
  }));
3380
3649
  } else if (eventName === "$paywall_viewed" || eventName === "$paywall_closed") {
3381
- console.warn("[Paywallo PAYWALL]", eventName, JSON.stringify({
3650
+ if (this.config?.debug) console.warn("[Paywallo PAYWALL]", eventName, JSON.stringify({
3382
3651
  distinctId: distinctId.substring(0, 8) + "...",
3383
3652
  placement: options?.properties?.placement,
3384
3653
  variantKey: options?.properties?.variantKey,
3385
3654
  timeOnPaywall: options?.properties?.timeOnPaywall
3386
3655
  }));
3387
3656
  } else {
3388
- 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) + "...");
3389
3658
  }
3390
3659
  await apiClient.trackEvent(eventName, distinctId, {
3391
3660
  ...identityManager.getProperties(),
@@ -3436,15 +3705,25 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3436
3705
  ...sessionId && { sessionId }
3437
3706
  });
3438
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
+ }
3439
3718
  async getVariant(flagKey) {
3440
3719
  try {
3441
3720
  const distinctId = identityManager.getDistinctId();
3442
3721
  if (!distinctId) {
3443
- 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);
3444
3723
  return { variant: null };
3445
3724
  }
3446
3725
  const result = await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
3447
- console.warn("[Paywallo AB]", `getVariant("${flagKey}") =`, result.variant);
3726
+ if (this.config?.debug) console.warn("[Paywallo AB]", `getVariant("${flagKey}") =`, result.variant);
3448
3727
  return result;
3449
3728
  } catch (error) {
3450
3729
  if (this.config?.debug) console.warn("[Paywallo]", "getVariant failed \u2014 returning null", error);
@@ -3452,6 +3731,10 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3452
3731
  return { variant: null };
3453
3732
  }
3454
3733
  }
3734
+ async evaluateFlags(keys) {
3735
+ this.ensureInitialized();
3736
+ return this.getApiClientOrThrow().evaluateFlags(keys, identityManager.getDistinctId());
3737
+ }
3455
3738
  async getConditionalFlag(flagKey, context) {
3456
3739
  try {
3457
3740
  const distinctId = identityManager.getDistinctId();
@@ -3477,7 +3760,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3477
3760
  }
3478
3761
  async presentCampaign(placement, context) {
3479
3762
  this.ensureInitialized();
3480
- 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);
3481
3764
  }
3482
3765
  async hasActiveSubscription() {
3483
3766
  this.ensureInitialized();
@@ -3485,11 +3768,12 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3485
3768
  if (this.activeChecker) return await this.activeChecker();
3486
3769
  const distinctId = identityManager.getDistinctId();
3487
3770
  if (!distinctId || !this.apiClient) return false;
3488
- return (await this.apiClient.getSubscriptionStatus(distinctId)).hasActiveSubscription;
3771
+ const status = await this.apiClient.getSubscriptionStatus(distinctId);
3772
+ return status.hasActiveSubscription;
3489
3773
  } catch {
3490
- 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");
3491
3775
  if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", "hasActiveSubscription failed");
3492
- return false;
3776
+ return resolveClientOfflineFromCache();
3493
3777
  }
3494
3778
  }
3495
3779
  async getSubscription() {
@@ -3518,6 +3802,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3518
3802
  const wasDebug = this.config?.debug ?? false;
3519
3803
  await sessionManager.endSession();
3520
3804
  await identityManager.reset();
3805
+ await subscriptionCache.invalidate();
3521
3806
  queueProcessor.dispose();
3522
3807
  offlineQueue.dispose();
3523
3808
  networkMonitor.dispose();
@@ -3874,86 +4159,6 @@ var import_react13 = require("react");
3874
4159
 
3875
4160
  // src/domains/subscription/SubscriptionManager.ts
3876
4161
  var import_react_native13 = require("react-native");
3877
-
3878
- // src/domains/subscription/SubscriptionCache.ts
3879
- var CACHE_KEY = "subscription_cache";
3880
- var DEFAULT_TTL = 5 * 60 * 1e3;
3881
- var SubscriptionCache = class {
3882
- constructor(ttl = DEFAULT_TTL) {
3883
- this.memoryCache = null;
3884
- this.ttl = ttl;
3885
- this.storage = new SecureStorage();
3886
- }
3887
- async get() {
3888
- if (this.memoryCache && !this.isExpired(this.memoryCache)) {
3889
- return this.memoryCache;
3890
- }
3891
- try {
3892
- const stored = await this.storage.get(CACHE_KEY);
3893
- if (!stored) {
3894
- return null;
3895
- }
3896
- const cached = JSON.parse(stored);
3897
- if (cached.data.subscription?.expiresAt) {
3898
- cached.data.subscription.expiresAt = new Date(cached.data.subscription.expiresAt);
3899
- }
3900
- if (cached.data.subscription?.originalPurchaseDate) {
3901
- cached.data.subscription.originalPurchaseDate = new Date(
3902
- cached.data.subscription.originalPurchaseDate
3903
- );
3904
- }
3905
- if (cached.data.subscription?.latestPurchaseDate) {
3906
- cached.data.subscription.latestPurchaseDate = new Date(
3907
- cached.data.subscription.latestPurchaseDate
3908
- );
3909
- }
3910
- if (cached.data.subscription?.cancellationDate) {
3911
- cached.data.subscription.cancellationDate = new Date(
3912
- cached.data.subscription.cancellationDate
3913
- );
3914
- }
3915
- if (cached.data.subscription?.gracePeriodExpiresAt) {
3916
- cached.data.subscription.gracePeriodExpiresAt = new Date(
3917
- cached.data.subscription.gracePeriodExpiresAt
3918
- );
3919
- }
3920
- cached.isStale = this.isExpired(cached);
3921
- if (!cached.isStale) {
3922
- this.memoryCache = cached;
3923
- }
3924
- return cached;
3925
- } catch {
3926
- return null;
3927
- }
3928
- }
3929
- async set(data) {
3930
- const cached = {
3931
- data,
3932
- cachedAt: Date.now(),
3933
- isStale: false
3934
- };
3935
- this.memoryCache = cached;
3936
- try {
3937
- await this.storage.set(CACHE_KEY, JSON.stringify(cached));
3938
- } catch {
3939
- }
3940
- }
3941
- async invalidate() {
3942
- this.memoryCache = null;
3943
- try {
3944
- await this.storage.remove(CACHE_KEY);
3945
- } catch {
3946
- }
3947
- }
3948
- isExpired(cached) {
3949
- return Date.now() - cached.cachedAt > this.ttl;
3950
- }
3951
- setTTL(ttl) {
3952
- this.ttl = ttl;
3953
- }
3954
- };
3955
-
3956
- // src/domains/subscription/SubscriptionManager.ts
3957
4162
  var SubscriptionManagerClass = class {
3958
4163
  constructor() {
3959
4164
  this.config = null;
@@ -4369,12 +4574,54 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4369
4574
  // src/hooks/useSubscriptionSync.ts
4370
4575
  var import_react17 = require("react");
4371
4576
  var CACHE_TTL_MS2 = 5 * 60 * 1e3;
4372
- 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
+ }
4373
4619
  function useSubscriptionSync() {
4374
4620
  const [subscription, setSubscription] = (0, import_react17.useState)(null);
4375
4621
  const cacheRef = (0, import_react17.useRef)(null);
4376
4622
  const invalidateCache = (0, import_react17.useCallback)(() => {
4377
4623
  cacheRef.current = null;
4624
+ void subscriptionCache.invalidate();
4378
4625
  }, []);
4379
4626
  const fetchAndCache = (0, import_react17.useCallback)(async () => {
4380
4627
  const apiClient = PaywalloClient.getApiClient();
@@ -4387,6 +4634,7 @@ function useSubscriptionSync() {
4387
4634
  } : null;
4388
4635
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4389
4636
  setSubscription(sub);
4637
+ void subscriptionCache.set(toDomainResponse(status));
4390
4638
  return sub;
4391
4639
  }, []);
4392
4640
  const hasActiveSubscription = (0, import_react17.useCallback)(async () => {
@@ -4395,10 +4643,28 @@ function useSubscriptionSync() {
4395
4643
  if (!apiClient || !distinctId) return false;
4396
4644
  const cached = cacheRef.current;
4397
4645
  if (cached && cached.expiresAt > Date.now()) {
4398
- if (!cached.data) return false;
4399
- if (!ACTIVE_STATUSES.includes(cached.data.status)) return false;
4400
- if (cached.data.expiresAt && cached.data.expiresAt < /* @__PURE__ */ new Date()) return false;
4401
- 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 {
4402
4668
  }
4403
4669
  try {
4404
4670
  const status = await apiClient.getSubscriptionStatus(distinctId);
@@ -4408,11 +4674,12 @@ function useSubscriptionSync() {
4408
4674
  } : null;
4409
4675
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4410
4676
  setSubscription(sub);
4677
+ void subscriptionCache.set(toDomainResponse(status));
4411
4678
  return status.hasActiveSubscription;
4412
4679
  } catch {
4413
- return false;
4680
+ return await resolveOfflineFromCache();
4414
4681
  }
4415
- }, []);
4682
+ }, [fetchAndCache]);
4416
4683
  const getSubscription = (0, import_react17.useCallback)(async () => {
4417
4684
  const apiClient = PaywalloClient.getApiClient();
4418
4685
  const distinctId = PaywalloClient.getDistinctId();
@@ -4422,7 +4689,22 @@ function useSubscriptionSync() {
4422
4689
  try {
4423
4690
  return await fetchAndCache();
4424
4691
  } catch {
4425
- 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
+ }
4426
4708
  }
4427
4709
  }, [fetchAndCache]);
4428
4710
  return { subscription, hasActiveSubscription, getSubscription, invalidateCache };