@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.mjs CHANGED
@@ -38,8 +38,17 @@ var CLIENT_ERROR_CODES = {
38
38
  };
39
39
 
40
40
  // src/domains/paywall/PaywallModal.tsx
41
- import { useEffect as useEffect3, useRef as useRef4 } from "react";
42
- import { Animated as Animated2, Dimensions as Dimensions2, Easing, Modal, StyleSheet as StyleSheet2, Text, View as View2 } from "react-native";
41
+ import { useCallback as useCallback4, useEffect as useEffect3, useRef as useRef4, useState as useState4 } from "react";
42
+ import {
43
+ Animated as Animated2,
44
+ Dimensions as Dimensions2,
45
+ Easing,
46
+ Modal,
47
+ Pressable,
48
+ StyleSheet as StyleSheet2,
49
+ Text,
50
+ View as View2
51
+ } from "react-native";
43
52
 
44
53
  // src/domains/paywall/usePaywallActions.ts
45
54
  import { useCallback as useCallback2, useRef, useState } from "react";
@@ -860,11 +869,6 @@ function usePaywallLoader(placement, visible, preloadedConfig, preloadedProducts
860
869
  const storeProducts = await getIAPService().loadProducts(productIds);
861
870
  const map = /* @__PURE__ */ new Map();
862
871
  for (const p of storeProducts) map.set(p.productId, p);
863
- if (PaywalloClient.getConfig()?.debug) {
864
- for (const [id, p] of map) {
865
- 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"}`);
866
- }
867
- }
868
872
  setProducts(map);
869
873
  } catch {
870
874
  setProducts(/* @__PURE__ */ new Map());
@@ -951,6 +955,13 @@ function getWebViewEmitter() {
951
955
  }
952
956
  return webViewEmitter;
953
957
  }
958
+ function parseErrorEvent(raw) {
959
+ return {
960
+ code: typeof raw["code"] === "number" ? raw["code"] : -1,
961
+ description: typeof raw["description"] === "string" ? raw["description"] : "WebView error",
962
+ url: typeof raw["url"] === "string" ? raw["url"] : void 0
963
+ };
964
+ }
954
965
  function PaywallWebView({
955
966
  paywallId,
956
967
  craftData,
@@ -961,11 +972,14 @@ function PaywallWebView({
961
972
  onPurchase,
962
973
  onClose,
963
974
  onRestore,
964
- onReady
975
+ onReady,
976
+ onError
965
977
  }) {
966
978
  const paywallDataSent = useRef3(false);
967
979
  const loadEndReceived = useRef3(false);
968
980
  const [scriptToInject, setScriptToInject] = useState3(void 0);
981
+ const onErrorRef = useRef3(onError);
982
+ onErrorRef.current = onError;
969
983
  const NativeWebView = useMemo(() => getNativeWebView(), []);
970
984
  const webUrl = useMemo(() => PaywalloClient.getWebUrl() ?? "http://localhost:3000", []);
971
985
  const buildAndSetInjectionScript = useCallback3(() => {
@@ -1025,17 +1039,51 @@ function PaywallWebView({
1025
1039
  const message = parseWebViewMessage(event.data);
1026
1040
  if (message) handleParsedMessage(message);
1027
1041
  });
1042
+ const errorSub = emitter.addListener(
1043
+ "PaywalloWebViewError",
1044
+ (event) => {
1045
+ onErrorRef.current?.(parseErrorEvent(event));
1046
+ }
1047
+ );
1028
1048
  return () => {
1029
1049
  loadEndSub.remove();
1030
1050
  messageSub.remove();
1051
+ errorSub.remove();
1031
1052
  };
1032
1053
  }, [buildAndSetInjectionScript, handleParsedMessage]);
1054
+ useEffect2(() => {
1055
+ const timer = setTimeout(() => {
1056
+ if (!paywallDataSent.current) {
1057
+ onErrorRef.current?.({
1058
+ code: -1001,
1059
+ description: "Paywall load timed out after 10 seconds"
1060
+ });
1061
+ }
1062
+ }, 1e4);
1063
+ return () => clearTimeout(timer);
1064
+ }, []);
1033
1065
  const handleLoadEnd = useCallback3(() => {
1034
1066
  if (!loadEndReceived.current) {
1035
1067
  loadEndReceived.current = true;
1036
1068
  buildAndSetInjectionScript();
1037
1069
  }
1038
1070
  }, [buildAndSetInjectionScript]);
1071
+ const handleNativeError = useCallback3((event) => {
1072
+ const ne = event.nativeEvent;
1073
+ let errorData;
1074
+ if (ne["payload"] && typeof ne["payload"] === "object") {
1075
+ errorData = ne["payload"];
1076
+ } else if (typeof ne["data"] === "string") {
1077
+ try {
1078
+ errorData = JSON.parse(ne["data"]);
1079
+ } catch {
1080
+ errorData = {};
1081
+ }
1082
+ } else {
1083
+ errorData = {};
1084
+ }
1085
+ onErrorRef.current?.(parseErrorEvent(errorData));
1086
+ }, []);
1039
1087
  const handleMessage = useCallback3(
1040
1088
  (event) => {
1041
1089
  const message = parseWebViewMessage(event.nativeEvent.data);
@@ -1055,6 +1103,7 @@ function PaywallWebView({
1055
1103
  injectedJavaScript: scriptToInject,
1056
1104
  onMessage: handleMessage,
1057
1105
  onLoadEnd: handleLoadEnd,
1106
+ onError: handleNativeError,
1058
1107
  style: styles.webview
1059
1108
  }
1060
1109
  ) });
@@ -1065,7 +1114,7 @@ var styles = StyleSheet.create({
1065
1114
  });
1066
1115
 
1067
1116
  // src/domains/paywall/PaywallModal.tsx
1068
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1117
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
1069
1118
  var SCREEN_HEIGHT2 = Dimensions2.get("window").height;
1070
1119
  function PaywallModal({
1071
1120
  placement,
@@ -1085,6 +1134,25 @@ function PaywallModal({
1085
1134
  campaignId
1086
1135
  );
1087
1136
  const { isPurchasing, slideAnim, handleClose, handlePurchase, handleRestore } = usePaywallActions(placement, paywallConfig, onResult, variantKey, campaignId);
1137
+ const [webViewError, setWebViewError] = useState4(null);
1138
+ const [retryCount, setRetryCount] = useState4(0);
1139
+ const retryCountRef = useRef4(0);
1140
+ const handleWebViewError = useCallback4(
1141
+ (err) => {
1142
+ if (retryCountRef.current >= 1) {
1143
+ setWebViewError({ code: err.code, description: err.description });
1144
+ } else {
1145
+ retryCountRef.current += 1;
1146
+ setRetryCount(retryCountRef.current);
1147
+ }
1148
+ },
1149
+ []
1150
+ );
1151
+ const handleRetry = useCallback4(() => {
1152
+ retryCountRef.current = 0;
1153
+ setWebViewError(null);
1154
+ setRetryCount(0);
1155
+ }, []);
1088
1156
  const openAnim = useRef4(new Animated2.Value(SCREEN_HEIGHT2)).current;
1089
1157
  useEffect3(() => {
1090
1158
  if (visible) {
@@ -1109,7 +1177,12 @@ function PaywallModal({
1109
1177
  statusBarTranslucent: true,
1110
1178
  children: /* @__PURE__ */ jsx2(View2, { style: styles2.overlay, children: /* @__PURE__ */ jsx2(Animated2.View, { style: [styles2.animatedContainer, { transform: [{ translateY: openAnim }] }], children: /* @__PURE__ */ jsxs(View2, { style: styles2.container, children: [
1111
1179
  error && /* @__PURE__ */ jsx2(View2, { style: styles2.errorContainer, children: /* @__PURE__ */ jsx2(Text, { style: styles2.errorText, children: error.message }) }),
1112
- !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ jsx2(
1180
+ !error && paywallConfig && "craftData" in paywallConfig.config && /* @__PURE__ */ jsx2(Fragment, { children: webViewError ? /* @__PURE__ */ jsx2(View2, { style: styles2.fallbackContainer, children: /* @__PURE__ */ jsxs(View2, { style: styles2.fallbackCard, children: [
1181
+ /* @__PURE__ */ jsx2(Text, { style: styles2.fallbackTitle, children: "Algo deu errado" }),
1182
+ /* @__PURE__ */ jsx2(Text, { style: styles2.fallbackDescription, children: webViewError.description }),
1183
+ /* @__PURE__ */ jsx2(Pressable, { style: styles2.retryButton, onPress: handleRetry, children: /* @__PURE__ */ jsx2(Text, { style: styles2.retryButtonText, children: "Tentar novamente" }) }),
1184
+ /* @__PURE__ */ jsx2(Pressable, { style: styles2.closeButton, onPress: handleClose, children: /* @__PURE__ */ jsx2(Text, { style: styles2.closeButtonText, children: "Fechar" }) })
1185
+ ] }) }) : /* @__PURE__ */ jsx2(
1113
1186
  PaywallWebView,
1114
1187
  {
1115
1188
  paywallId: paywallConfig.id,
@@ -1120,39 +1193,67 @@ function PaywallModal({
1120
1193
  isPurchasing,
1121
1194
  onClose: handleClose,
1122
1195
  onPurchase: handlePurchase,
1123
- onRestore: handleRestore
1124
- }
1125
- )
1196
+ onRestore: handleRestore,
1197
+ onError: handleWebViewError
1198
+ },
1199
+ retryCount
1200
+ ) })
1126
1201
  ] }) }) })
1127
1202
  }
1128
1203
  );
1129
1204
  }
1130
1205
  var styles2 = StyleSheet2.create({
1131
- overlay: {
1206
+ overlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.3)" },
1207
+ animatedContainer: { flex: 1, backgroundColor: "#fff" },
1208
+ container: { flex: 1, backgroundColor: "#fff" },
1209
+ errorContainer: { flex: 1, justifyContent: "center", alignItems: "center", padding: 20 },
1210
+ errorText: { fontSize: 16, color: "#ef4444", textAlign: "center" },
1211
+ fallbackContainer: {
1132
1212
  flex: 1,
1133
- backgroundColor: "rgba(0,0,0,0.3)"
1213
+ justifyContent: "center",
1214
+ alignItems: "center",
1215
+ backgroundColor: "#111010",
1216
+ padding: 24
1134
1217
  },
1135
- animatedContainer: {
1136
- flex: 1,
1137
- backgroundColor: "#fff",
1138
- borderTopLeftRadius: 0,
1139
- borderTopRightRadius: 0
1218
+ fallbackCard: {
1219
+ width: "100%",
1220
+ maxWidth: 360,
1221
+ alignItems: "center",
1222
+ backgroundColor: "rgba(255,255,255,0.07)",
1223
+ borderRadius: 16,
1224
+ padding: 28
1140
1225
  },
1141
- container: {
1142
- flex: 1,
1143
- backgroundColor: "#fff"
1226
+ fallbackTitle: {
1227
+ fontSize: 18,
1228
+ fontWeight: "600",
1229
+ color: "#f5f5f5",
1230
+ marginBottom: 10,
1231
+ textAlign: "center"
1144
1232
  },
1145
- errorContainer: {
1146
- flex: 1,
1147
- justifyContent: "center",
1233
+ fallbackDescription: {
1234
+ fontSize: 14,
1235
+ color: "rgba(255,255,255,0.55)",
1236
+ textAlign: "center",
1237
+ marginBottom: 28,
1238
+ lineHeight: 20
1239
+ },
1240
+ retryButton: {
1241
+ width: "100%",
1242
+ backgroundColor: "#ffffff",
1243
+ borderRadius: 10,
1244
+ paddingVertical: 14,
1148
1245
  alignItems: "center",
1149
- padding: 20
1246
+ marginBottom: 12
1150
1247
  },
1151
- errorText: {
1152
- fontSize: 16,
1153
- color: "#ef4444",
1154
- textAlign: "center"
1155
- }
1248
+ retryButtonText: { fontSize: 15, fontWeight: "600", color: "#111010" },
1249
+ closeButton: {
1250
+ width: "100%",
1251
+ borderRadius: 10,
1252
+ paddingVertical: 14,
1253
+ alignItems: "center",
1254
+ backgroundColor: "rgba(255,255,255,0.1)"
1255
+ },
1256
+ closeButtonText: { fontSize: 15, fontWeight: "500", color: "rgba(255,255,255,0.7)" }
1156
1257
  });
1157
1258
 
1158
1259
  // src/domains/paywall/PaywallErrorBoundary.tsx
@@ -1188,9 +1289,9 @@ function usePaywallContext() {
1188
1289
  }
1189
1290
 
1190
1291
  // src/domains/paywall/usePaywallPresenter.ts
1191
- import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef5, useState as useState4 } from "react";
1292
+ import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef5, useState as useState5 } from "react";
1192
1293
  function usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreloadedWebView) {
1193
- const [showingPreloadedWebView, setShowingPreloadedWebView] = useState4(false);
1294
+ const [showingPreloadedWebView, setShowingPreloadedWebView] = useState5(false);
1194
1295
  const resolverRef = useRef5(null);
1195
1296
  const presentedAtRef = useRef5(Date.now());
1196
1297
  useEffect4(() => {
@@ -1201,23 +1302,23 @@ function usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreload
1201
1302
  }
1202
1303
  };
1203
1304
  }, []);
1204
- const presentPreloadedWebView = useCallback4(() => {
1305
+ const presentPreloadedWebView = useCallback5(() => {
1205
1306
  return new Promise((resolve) => {
1206
1307
  resolverRef.current = resolve;
1207
1308
  presentedAtRef.current = Date.now();
1208
1309
  setShowingPreloadedWebView(true);
1209
1310
  });
1210
1311
  }, []);
1211
- const resolvePreloadedPaywall = useCallback4((result) => {
1312
+ const resolvePreloadedPaywall = useCallback5((result) => {
1212
1313
  if (resolverRef.current) {
1213
1314
  resolverRef.current(result);
1214
1315
  resolverRef.current = null;
1215
1316
  }
1216
1317
  }, []);
1217
- const handlePreloadedWebViewReady = useCallback4(() => {
1318
+ const handlePreloadedWebViewReady = useCallback5(() => {
1218
1319
  setPreloadedWebView((prev) => prev ? { ...prev, loaded: true } : null);
1219
1320
  }, [setPreloadedWebView]);
1220
- const handlePreloadedClose = useCallback4(() => {
1321
+ const handlePreloadedClose = useCallback5(() => {
1221
1322
  const track = async () => {
1222
1323
  try {
1223
1324
  const apiClient = PaywalloClient.getApiClient();
@@ -1467,6 +1568,92 @@ var PaywalloAffiliate = {
1467
1568
  getWithdrawals: () => affiliateManager.getWithdrawals()
1468
1569
  };
1469
1570
 
1571
+ // src/domains/subscription/SubscriptionCache.ts
1572
+ var CACHE_KEY = "subscription_cache";
1573
+ var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
1574
+ var SubscriptionCache = class {
1575
+ constructor(ttl = DEFAULT_TTL) {
1576
+ this.memoryCache = null;
1577
+ this.ttl = ttl;
1578
+ this.storage = new SecureStorage();
1579
+ }
1580
+ async get() {
1581
+ if (this.memoryCache && !this.isExpired(this.memoryCache)) {
1582
+ return this.memoryCache;
1583
+ }
1584
+ try {
1585
+ const stored = await this.storage.get(CACHE_KEY);
1586
+ if (!stored) {
1587
+ return null;
1588
+ }
1589
+ const parsed = JSON.parse(stored);
1590
+ if (typeof parsed !== "object" || parsed === null || !("data" in parsed) || typeof parsed.data !== "object" || parsed.data === null || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") {
1591
+ await this.storage.remove(CACHE_KEY);
1592
+ return null;
1593
+ }
1594
+ const cached = parsed;
1595
+ if (cached.data.subscription?.expiresAt) {
1596
+ cached.data.subscription.expiresAt = new Date(cached.data.subscription.expiresAt);
1597
+ }
1598
+ if (cached.data.subscription?.originalPurchaseDate) {
1599
+ cached.data.subscription.originalPurchaseDate = new Date(
1600
+ cached.data.subscription.originalPurchaseDate
1601
+ );
1602
+ }
1603
+ if (cached.data.subscription?.latestPurchaseDate) {
1604
+ cached.data.subscription.latestPurchaseDate = new Date(
1605
+ cached.data.subscription.latestPurchaseDate
1606
+ );
1607
+ }
1608
+ if (cached.data.subscription?.cancellationDate) {
1609
+ cached.data.subscription.cancellationDate = new Date(
1610
+ cached.data.subscription.cancellationDate
1611
+ );
1612
+ }
1613
+ if (cached.data.subscription?.gracePeriodExpiresAt) {
1614
+ cached.data.subscription.gracePeriodExpiresAt = new Date(
1615
+ cached.data.subscription.gracePeriodExpiresAt
1616
+ );
1617
+ }
1618
+ cached.isStale = this.isExpired(cached);
1619
+ if (!cached.isStale) {
1620
+ this.memoryCache = cached;
1621
+ }
1622
+ return cached;
1623
+ } catch {
1624
+ return null;
1625
+ }
1626
+ }
1627
+ async set(data) {
1628
+ const cached = {
1629
+ data,
1630
+ cachedAt: Date.now(),
1631
+ isStale: false
1632
+ };
1633
+ this.memoryCache = cached;
1634
+ try {
1635
+ await this.storage.set(CACHE_KEY, JSON.stringify(cached));
1636
+ } catch (error) {
1637
+ if (__DEV__) console.warn("[SubscriptionCache] set() failed:", error instanceof Error ? error.message : String(error));
1638
+ }
1639
+ }
1640
+ async invalidate() {
1641
+ this.memoryCache = null;
1642
+ try {
1643
+ await this.storage.remove(CACHE_KEY);
1644
+ } catch (error) {
1645
+ if (__DEV__) console.warn("[SubscriptionCache] invalidate() failed:", error instanceof Error ? error.message : String(error));
1646
+ }
1647
+ }
1648
+ isExpired(cached) {
1649
+ return Date.now() - cached.cachedAt > this.ttl;
1650
+ }
1651
+ setTTL(ttl) {
1652
+ this.ttl = ttl;
1653
+ }
1654
+ };
1655
+ var subscriptionCache = new SubscriptionCache();
1656
+
1470
1657
  // src/core/ApiClient.ts
1471
1658
  import { Platform as Platform3 } from "react-native";
1472
1659
 
@@ -2329,7 +2516,7 @@ var ApiCache = class {
2329
2516
  // src/core/ApiClient.ts
2330
2517
  var SDK_VERSION = "1.0.0";
2331
2518
  var DEFAULT_BASE_URL = "https://panel.lucasqueiroga.shop";
2332
- var ApiClient = class {
2519
+ var _ApiClient = class _ApiClient {
2333
2520
  constructor(appKey, debug = false, environment = "Production", offlineQueueEnabled = true, timeout = 3e4) {
2334
2521
  this.cache = new ApiCache();
2335
2522
  this.appKey = appKey;
@@ -2419,6 +2606,11 @@ var ApiClient = class {
2419
2606
  const key = `${flagKey}:${distinctId}`;
2420
2607
  const hit = this.cache.getVariant(key);
2421
2608
  if (hit !== void 0) return hit;
2609
+ const persisted = await this.readFlagFromStorage(flagKey, distinctId);
2610
+ if (persisted !== null) {
2611
+ this.cache.setVariant(key, persisted);
2612
+ return persisted;
2613
+ }
2422
2614
  try {
2423
2615
  const res = await this.get(`/api/v1/flags/${flagKey}?distinctId=${encodeURIComponent(distinctId)}`);
2424
2616
  if (res.status === 404) {
@@ -2433,6 +2625,7 @@ var ApiClient = class {
2433
2625
  return { variant: null };
2434
2626
  }
2435
2627
  this.cache.setVariant(key, res.data);
2628
+ await this.writeFlagToStorage(flagKey, distinctId, res.data);
2436
2629
  return res.data;
2437
2630
  } catch (error) {
2438
2631
  this.log("Failed to get variant:", flagKey, error);
@@ -2514,6 +2707,35 @@ var ApiClient = class {
2514
2707
  this.log("Subscription status:", result.hasActiveSubscription);
2515
2708
  return result;
2516
2709
  }
2710
+ async evaluateFlags(keys, distinctId) {
2711
+ const query = keys.map(encodeURIComponent).join(",");
2712
+ const path = `/api/v1/flags/evaluate?keys=${query}`;
2713
+ try {
2714
+ const res = await this.httpClient.get(path, {
2715
+ headers: { "X-App-Key": this.appKey, "x-distinct-id": distinctId }
2716
+ });
2717
+ if (!res.ok) {
2718
+ this.log("Non-OK response for evaluateFlags:", res.status);
2719
+ return {};
2720
+ }
2721
+ const raw = res.data;
2722
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
2723
+ const record = raw;
2724
+ const result = {};
2725
+ for (const key of Object.keys(record)) {
2726
+ const value = record[key];
2727
+ const variant = typeof value === "boolean" ? String(value) : typeof value === "string" ? value : null;
2728
+ result[key] = variant;
2729
+ const flagVariant = { variant };
2730
+ this.cache.setVariant(`${key}:${distinctId}`, flagVariant);
2731
+ await this.writeFlagToStorage(key, distinctId, flagVariant);
2732
+ }
2733
+ return result;
2734
+ } catch (error) {
2735
+ this.log("Failed to evaluateFlags:", error);
2736
+ return {};
2737
+ }
2738
+ }
2517
2739
  async getConditionalFlag(flagKey, context) {
2518
2740
  try {
2519
2741
  const url = buildConditionalFlagUrl(this.baseUrl, flagKey, context);
@@ -2530,7 +2752,43 @@ var ApiClient = class {
2530
2752
  return { value: false, flagKey };
2531
2753
  }
2532
2754
  }
2533
- // ─── Private helpers ─────────────────────────────────────────────────────────
2755
+ // ─── Cache-only reads ────────────────────────────────────────────────────────
2756
+ /**
2757
+ * Reads a flag variant from cache layers only — no network call.
2758
+ * Checks in-memory cache first, then SecureStorage.
2759
+ * Returns null when both layers miss.
2760
+ */
2761
+ async getVariantFromCache(flagKey, distinctId) {
2762
+ const key = `${flagKey}:${distinctId}`;
2763
+ const hit = this.cache.getVariant(key);
2764
+ if (hit !== void 0) return hit;
2765
+ const persisted = await this.readFlagFromStorage(flagKey, distinctId);
2766
+ if (persisted !== null) {
2767
+ this.cache.setVariant(key, persisted);
2768
+ return persisted;
2769
+ }
2770
+ return null;
2771
+ }
2772
+ async readFlagFromStorage(flagKey, distinctId) {
2773
+ try {
2774
+ const raw = await secureStorage.get(`flag:${flagKey}:${distinctId}`);
2775
+ if (!raw) return null;
2776
+ const parsed = JSON.parse(raw);
2777
+ if (typeof parsed !== "object" || parsed === null || !("variant" in parsed) || !("cachedAt" in parsed) || typeof parsed.cachedAt !== "number") return null;
2778
+ const { variant, payload, cachedAt } = parsed;
2779
+ if (Date.now() - cachedAt > _ApiClient.FLAG_MAX_AGE_MS) return null;
2780
+ return { variant, payload };
2781
+ } catch {
2782
+ return null;
2783
+ }
2784
+ }
2785
+ async writeFlagToStorage(flagKey, distinctId, flag) {
2786
+ try {
2787
+ const value = JSON.stringify({ variant: flag.variant, payload: flag.payload, cachedAt: Date.now() });
2788
+ await secureStorage.set(`flag:${flagKey}:${distinctId}`, value);
2789
+ } catch {
2790
+ }
2791
+ }
2534
2792
  /**
2535
2793
  * POST with offline queue fallback. Enqueues when offline or on network failure.
2536
2794
  * Throws only when offlineQueueEnabled is false and the request fails.
@@ -2557,6 +2815,9 @@ var ApiClient = class {
2557
2815
  if (this.debug) console.warn("[Paywallo API]", ...args);
2558
2816
  }
2559
2817
  };
2818
+ // ─── Private helpers ─────────────────────────────────────────────────────────
2819
+ _ApiClient.FLAG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
2820
+ var ApiClient = _ApiClient;
2560
2821
 
2561
2822
  // src/domains/campaign/CampaignError.ts
2562
2823
  var CampaignError = class extends PaywalloError {
@@ -2584,18 +2845,21 @@ var CampaignGateService = class {
2584
2845
  if (!distinctId) return null;
2585
2846
  return apiClient.getCampaign(placement, distinctId, context);
2586
2847
  }
2587
- async presentCampaign(apiClient, placement, paywallPresenter, campaignPresenter, context) {
2848
+ async presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
2588
2849
  const campaign = await this.getCampaign(apiClient, placement, context);
2589
2850
  if (!campaign) {
2590
2851
  return { presented: false, purchased: false, cancelled: false, restored: false, error: new CampaignError(CAMPAIGN_ERROR_CODES.NOT_FOUND, "No campaign found") };
2591
2852
  }
2853
+ if (!context?.forceShow && await hasActive()) {
2854
+ return { presented: false, purchased: false, cancelled: false, restored: false, skippedReason: "subscriber" };
2855
+ }
2592
2856
  if (campaignPresenter) return campaignPresenter(campaign);
2593
2857
  if (!paywallPresenter) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "PaywalloProvider not mounted. Wrap your app with <PaywalloProvider>");
2594
2858
  return { ...await paywallPresenter(campaign.paywall.placement), campaignId: campaign.campaignId, variantKey: campaign.variantKey };
2595
2859
  }
2596
2860
  async requireSubscriptionWithCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
2597
2861
  if (await hasActive()) return true;
2598
- const r = await this.presentCampaign(apiClient, placement, paywallPresenter, campaignPresenter, context);
2862
+ const r = await this.presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context);
2599
2863
  return r.purchased || r.restored;
2600
2864
  }
2601
2865
  async preloadCampaign(apiClient, placement, context) {
@@ -3132,6 +3396,20 @@ function isValidEventName(name) {
3132
3396
  if (name.startsWith("$")) return /^\$[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
3133
3397
  return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
3134
3398
  }
3399
+ var OFFLINE_ACTIVE_STATUSES = ["active", "grace_period"];
3400
+ async function resolveClientOfflineFromCache() {
3401
+ try {
3402
+ const cached = await subscriptionCache.get();
3403
+ if (!cached) return false;
3404
+ const sub = cached.data.subscription;
3405
+ if (!sub) return false;
3406
+ if (!OFFLINE_ACTIVE_STATUSES.includes(sub.status)) return false;
3407
+ if (!sub.expiresAt) return true;
3408
+ return sub.expiresAt > /* @__PURE__ */ new Date();
3409
+ } catch {
3410
+ return false;
3411
+ }
3412
+ }
3135
3413
  var _PaywalloClientClass = class _PaywalloClientClass {
3136
3414
  constructor() {
3137
3415
  this.config = null;
@@ -3148,27 +3426,27 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3148
3426
  this.initAttempts = 0;
3149
3427
  }
3150
3428
  async init(config) {
3151
- console.warn("[Paywallo INIT]", "init() called \u2014 appKey:", config.appKey ? config.appKey.substring(0, 8) + "..." : "MISSING");
3429
+ if (config.debug) console.warn("[Paywallo INIT]", "init() called \u2014 appKey:", config.appKey ? config.appKey.substring(0, 8) + "..." : "MISSING");
3152
3430
  if (this.config && this.apiClient) {
3153
- console.warn("[Paywallo INIT]", "Already initialized \u2014 skipping");
3431
+ if (config.debug) console.warn("[Paywallo INIT]", "Already initialized \u2014 skipping");
3154
3432
  return;
3155
3433
  }
3156
3434
  if (this.initPromise) {
3157
- console.warn("[Paywallo INIT]", "Init already in progress \u2014 awaiting...");
3435
+ if (config.debug) console.warn("[Paywallo INIT]", "Init already in progress \u2014 awaiting...");
3158
3436
  await this.initPromise;
3159
- console.warn("[Paywallo INIT]", "Awaited existing init \u2014 done");
3437
+ if (config.debug) console.warn("[Paywallo INIT]", "Awaited existing init \u2014 done");
3160
3438
  return;
3161
3439
  }
3162
3440
  if (!config.appKey) throw new ClientError(CLIENT_ERROR_CODES.MISSING_APP_KEY, "appKey is required");
3163
- console.warn("[Paywallo INIT]", "Starting fresh init...");
3441
+ if (config.debug) console.warn("[Paywallo INIT]", "Starting fresh init...");
3164
3442
  this.pendingConfig = config;
3165
3443
  this.initAttempts = 0;
3166
3444
  this.initPromise = this.initWithRetry(config);
3167
3445
  try {
3168
3446
  await this.initPromise;
3169
- console.warn("[Paywallo INIT]", "init() SUCCESS");
3447
+ if (config.debug) console.warn("[Paywallo INIT]", "init() SUCCESS");
3170
3448
  } catch (error) {
3171
- console.warn("[Paywallo INIT]", "init() FAILED:", error instanceof Error ? error.message : String(error));
3449
+ if (config.debug) console.warn("[Paywallo INIT]", "init() FAILED:", error instanceof Error ? error.message : String(error));
3172
3450
  this.initPromise = null;
3173
3451
  this.config = null;
3174
3452
  this.apiClient = null;
@@ -3238,26 +3516,26 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3238
3516
  async doInit(config) {
3239
3517
  const environment = config.environment ?? (typeof __DEV__ !== "undefined" && __DEV__ ? "Sandbox" : "Production");
3240
3518
  const offlineQueueEnabled = config.offlineQueueEnabled !== false;
3241
- console.warn("[Paywallo INIT]", "Step 1: networkMonitor + offlineQueue...");
3519
+ if (config.debug) console.warn("[Paywallo INIT]", "Step 1: networkMonitor + offlineQueue...");
3242
3520
  await Promise.all([
3243
3521
  networkMonitor.initialize({ debug: config.debug }),
3244
3522
  offlineQueue.initialize({ debug: config.debug }).then(
3245
3523
  () => offlineQueue.clearItemsWithInvalidAppKey(config.appKey)
3246
3524
  )
3247
3525
  ]);
3248
- console.warn("[Paywallo INIT]", "Step 1 DONE");
3249
- console.warn("[Paywallo INIT]", "Step 2: ApiClient + queueProcessor + affiliate...");
3526
+ if (config.debug) console.warn("[Paywallo INIT]", "Step 1 DONE");
3527
+ if (config.debug) console.warn("[Paywallo INIT]", "Step 2: ApiClient + queueProcessor + affiliate...");
3250
3528
  this.apiClient = new ApiClient(config.appKey, config.debug, environment, offlineQueueEnabled, config.timeout);
3251
3529
  queueProcessor.initialize(this.apiClient.getHttpClient(), { debug: config.debug });
3252
3530
  affiliateManager.initialize(this.apiClient, config.debug);
3253
- console.warn("[Paywallo INIT]", "Step 2 DONE");
3254
- console.warn("[Paywallo INIT]", "Step 3: identityManager + sessionManager...");
3531
+ if (config.debug) console.warn("[Paywallo INIT]", "Step 2 DONE");
3532
+ if (config.debug) console.warn("[Paywallo INIT]", "Step 3: identityManager + sessionManager...");
3255
3533
  await Promise.all([
3256
3534
  identityManager.initialize(this.apiClient, config.debug),
3257
3535
  sessionManager.initialize(this.apiClient, config.sessionConfig, config.debug)
3258
3536
  ]);
3259
3537
  const distinctId = identityManager.getDistinctId();
3260
- console.warn("[Paywallo INIT]", "Step 3 DONE \u2014 distinctId:", distinctId ? distinctId.substring(0, 8) + "..." : "EMPTY");
3538
+ if (config.debug) console.warn("[Paywallo INIT]", "Step 3 DONE \u2014 distinctId:", distinctId ? distinctId.substring(0, 8) + "..." : "EMPTY");
3261
3539
  if (config.autoStartSession !== false) {
3262
3540
  sessionManager.startSession().catch(() => {
3263
3541
  if (config.debug) console.warn("[Paywallo]", "Session start failed during init \u2014 continuing");
@@ -3266,7 +3544,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3266
3544
  new InstallTracker(config.debug).trackIfNeeded(this.apiClient, config.requestATT === true).catch(() => {
3267
3545
  });
3268
3546
  this.config = config;
3269
- console.warn("[Paywallo INIT]", "COMPLETE \u2014 SDK ready, environment:", environment);
3547
+ if (config.debug) console.warn("[Paywallo INIT]", "COMPLETE \u2014 SDK ready, environment:", environment);
3270
3548
  }
3271
3549
  async identify(options) {
3272
3550
  this.ensureInitialized();
@@ -3276,7 +3554,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3276
3554
  const apiClient = this.getApiClientOrThrow();
3277
3555
  const distinctId = identityManager.getDistinctId();
3278
3556
  if (!distinctId) {
3279
- console.warn("[Paywallo TRACK]", "SKIPPED \u2014 no distinctId", eventName);
3557
+ if (this.config?.debug) console.warn("[Paywallo TRACK]", "SKIPPED \u2014 no distinctId", eventName);
3280
3558
  return;
3281
3559
  }
3282
3560
  if (!isValidEventName(eventName)) {
@@ -3285,7 +3563,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3285
3563
  }
3286
3564
  const sessionId = sessionManager.getSessionId();
3287
3565
  if (eventName === "$purchase_completed") {
3288
- console.warn("[Paywallo PURCHASE]", "tracking purchase:", JSON.stringify({
3566
+ if (this.config?.debug) console.warn("[Paywallo PURCHASE]", "tracking purchase:", JSON.stringify({
3289
3567
  distinctId: distinctId.substring(0, 8) + "...",
3290
3568
  productId: options?.properties?.productId,
3291
3569
  priceUsd: options?.properties?.priceUsd,
@@ -3294,14 +3572,14 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3294
3572
  placement: options?.properties?.placement
3295
3573
  }));
3296
3574
  } else if (eventName === "$paywall_viewed" || eventName === "$paywall_closed") {
3297
- console.warn("[Paywallo PAYWALL]", eventName, JSON.stringify({
3575
+ if (this.config?.debug) console.warn("[Paywallo PAYWALL]", eventName, JSON.stringify({
3298
3576
  distinctId: distinctId.substring(0, 8) + "...",
3299
3577
  placement: options?.properties?.placement,
3300
3578
  variantKey: options?.properties?.variantKey,
3301
3579
  timeOnPaywall: options?.properties?.timeOnPaywall
3302
3580
  }));
3303
3581
  } else {
3304
- console.warn("[Paywallo TRACK]", eventName, "distinctId:", distinctId.substring(0, 8) + "...");
3582
+ if (this.config?.debug) console.warn("[Paywallo TRACK]", eventName, "distinctId:", distinctId.substring(0, 8) + "...");
3305
3583
  }
3306
3584
  await apiClient.trackEvent(eventName, distinctId, {
3307
3585
  ...identityManager.getProperties(),
@@ -3352,15 +3630,25 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3352
3630
  ...sessionId && { sessionId }
3353
3631
  });
3354
3632
  }
3633
+ async getVariantCached(flagKey, defaultValue) {
3634
+ this.ensureInitialized();
3635
+ const apiClient = this.getApiClientOrThrow();
3636
+ const distinctId = identityManager.getDistinctId();
3637
+ const result = await apiClient.getVariantFromCache(flagKey, distinctId);
3638
+ if (result) return result;
3639
+ void apiClient.getVariant(flagKey, distinctId).catch(() => {
3640
+ });
3641
+ return { variant: defaultValue ?? null, payload: void 0 };
3642
+ }
3355
3643
  async getVariant(flagKey) {
3356
3644
  try {
3357
3645
  const distinctId = identityManager.getDistinctId();
3358
3646
  if (!distinctId) {
3359
- console.warn("[Paywallo AB]", "getVariant SKIPPED \u2014 no distinctId", flagKey);
3647
+ if (this.config?.debug) console.warn("[Paywallo AB]", "getVariant SKIPPED \u2014 no distinctId", flagKey);
3360
3648
  return { variant: null };
3361
3649
  }
3362
3650
  const result = await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
3363
- console.warn("[Paywallo AB]", `getVariant("${flagKey}") =`, result.variant);
3651
+ if (this.config?.debug) console.warn("[Paywallo AB]", `getVariant("${flagKey}") =`, result.variant);
3364
3652
  return result;
3365
3653
  } catch (error) {
3366
3654
  if (this.config?.debug) console.warn("[Paywallo]", "getVariant failed \u2014 returning null", error);
@@ -3368,6 +3656,10 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3368
3656
  return { variant: null };
3369
3657
  }
3370
3658
  }
3659
+ async evaluateFlags(keys) {
3660
+ this.ensureInitialized();
3661
+ return this.getApiClientOrThrow().evaluateFlags(keys, identityManager.getDistinctId());
3662
+ }
3371
3663
  async getConditionalFlag(flagKey, context) {
3372
3664
  try {
3373
3665
  const distinctId = identityManager.getDistinctId();
@@ -3393,7 +3685,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3393
3685
  }
3394
3686
  async presentCampaign(placement, context) {
3395
3687
  this.ensureInitialized();
3396
- return campaignGateService.presentCampaign(this.getApiClientOrThrow(), placement, this.paywallPresenter, this.campaignPresenter, context);
3688
+ return campaignGateService.presentCampaign(this.getApiClientOrThrow(), placement, () => this.hasActiveSubscription(), this.paywallPresenter, this.campaignPresenter, context);
3397
3689
  }
3398
3690
  async hasActiveSubscription() {
3399
3691
  this.ensureInitialized();
@@ -3401,11 +3693,12 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3401
3693
  if (this.activeChecker) return await this.activeChecker();
3402
3694
  const distinctId = identityManager.getDistinctId();
3403
3695
  if (!distinctId || !this.apiClient) return false;
3404
- return (await this.apiClient.getSubscriptionStatus(distinctId)).hasActiveSubscription;
3696
+ const status = await this.apiClient.getSubscriptionStatus(distinctId);
3697
+ return status.hasActiveSubscription;
3405
3698
  } catch {
3406
- if (this.config?.debug) console.warn("[Paywallo]", "hasActiveSubscription failed \u2014 returning false");
3699
+ if (this.config?.debug) console.warn("[Paywallo]", "hasActiveSubscription failed \u2014 using persistent cache");
3407
3700
  if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", "hasActiveSubscription failed");
3408
- return false;
3701
+ return resolveClientOfflineFromCache();
3409
3702
  }
3410
3703
  }
3411
3704
  async getSubscription() {
@@ -3434,6 +3727,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3434
3727
  const wasDebug = this.config?.debug ?? false;
3435
3728
  await sessionManager.endSession();
3436
3729
  await identityManager.reset();
3730
+ await subscriptionCache.invalidate();
3437
3731
  queueProcessor.dispose();
3438
3732
  offlineQueue.dispose();
3439
3733
  networkMonitor.dispose();
@@ -3621,7 +3915,7 @@ function usePaywallo() {
3621
3915
  }
3622
3916
 
3623
3917
  // src/hooks/useProducts.ts
3624
- import { useCallback as useCallback5, useEffect as useEffect5, useState as useState5 } from "react";
3918
+ import { useCallback as useCallback6, useEffect as useEffect5, useState as useState6 } from "react";
3625
3919
 
3626
3920
  // src/utils/productMappers.ts
3627
3921
  function mapToIAPProduct(product) {
@@ -3658,11 +3952,11 @@ function mapToFormattedProduct(product) {
3658
3952
 
3659
3953
  // src/hooks/useProducts.ts
3660
3954
  function useProducts(initialProductIds) {
3661
- const [products, setProducts] = useState5([]);
3662
- const [formattedProducts, setFormattedProducts] = useState5([]);
3663
- const [isLoading, setIsLoading] = useState5(false);
3664
- const [error, setError] = useState5(null);
3665
- const loadProducts = useCallback5(async (productIds) => {
3955
+ const [products, setProducts] = useState6([]);
3956
+ const [formattedProducts, setFormattedProducts] = useState6([]);
3957
+ const [isLoading, setIsLoading] = useState6(false);
3958
+ const [error, setError] = useState6(null);
3959
+ const loadProducts = useCallback6(async (productIds) => {
3666
3960
  setIsLoading(true);
3667
3961
  setError(null);
3668
3962
  try {
@@ -3681,19 +3975,19 @@ function useProducts(initialProductIds) {
3681
3975
  void loadProducts(initialProductIds);
3682
3976
  }
3683
3977
  }, []);
3684
- const getProduct = useCallback5(
3978
+ const getProduct = useCallback6(
3685
3979
  (productId) => {
3686
3980
  return products.find((p) => p.productId === productId);
3687
3981
  },
3688
3982
  [products]
3689
3983
  );
3690
- const getFormattedProduct = useCallback5(
3984
+ const getFormattedProduct = useCallback6(
3691
3985
  (productId) => {
3692
3986
  return formattedProducts.find((p) => p.productId === productId);
3693
3987
  },
3694
3988
  [formattedProducts]
3695
3989
  );
3696
- const getPriceInfo = useCallback5(
3990
+ const getPriceInfo = useCallback6(
3697
3991
  (productId) => {
3698
3992
  const p = formattedProducts.find((fp) => fp.productId === productId);
3699
3993
  if (!p) return void 0;
@@ -3722,11 +4016,11 @@ function useProducts(initialProductIds) {
3722
4016
  }
3723
4017
 
3724
4018
  // src/hooks/usePurchase.ts
3725
- import { useCallback as useCallback6, useState as useState6 } from "react";
4019
+ import { useCallback as useCallback7, useState as useState7 } from "react";
3726
4020
  function usePurchase() {
3727
- const [state, setState] = useState6("idle");
3728
- const [error, setError] = useState6(null);
3729
- const purchase = useCallback6(async (productId) => {
4021
+ const [state, setState] = useState7("idle");
4022
+ const [error, setError] = useState7(null);
4023
+ const purchase = useCallback7(async (productId) => {
3730
4024
  setState("purchasing");
3731
4025
  setError(null);
3732
4026
  try {
@@ -3754,7 +4048,7 @@ function usePurchase() {
3754
4048
  throw err;
3755
4049
  }
3756
4050
  }, []);
3757
- const restore = useCallback6(async () => {
4051
+ const restore = useCallback7(async () => {
3758
4052
  setState("restoring");
3759
4053
  setError(null);
3760
4054
  try {
@@ -3786,90 +4080,10 @@ function usePurchase() {
3786
4080
  }
3787
4081
 
3788
4082
  // src/hooks/useSubscription.ts
3789
- import { useCallback as useCallback7, useEffect as useEffect6, useState as useState7 } from "react";
4083
+ import { useCallback as useCallback8, useEffect as useEffect6, useState as useState8 } from "react";
3790
4084
 
3791
4085
  // src/domains/subscription/SubscriptionManager.ts
3792
4086
  import { Platform as Platform8 } from "react-native";
3793
-
3794
- // src/domains/subscription/SubscriptionCache.ts
3795
- var CACHE_KEY = "subscription_cache";
3796
- var DEFAULT_TTL = 5 * 60 * 1e3;
3797
- var SubscriptionCache = class {
3798
- constructor(ttl = DEFAULT_TTL) {
3799
- this.memoryCache = null;
3800
- this.ttl = ttl;
3801
- this.storage = new SecureStorage();
3802
- }
3803
- async get() {
3804
- if (this.memoryCache && !this.isExpired(this.memoryCache)) {
3805
- return this.memoryCache;
3806
- }
3807
- try {
3808
- const stored = await this.storage.get(CACHE_KEY);
3809
- if (!stored) {
3810
- return null;
3811
- }
3812
- const cached = JSON.parse(stored);
3813
- if (cached.data.subscription?.expiresAt) {
3814
- cached.data.subscription.expiresAt = new Date(cached.data.subscription.expiresAt);
3815
- }
3816
- if (cached.data.subscription?.originalPurchaseDate) {
3817
- cached.data.subscription.originalPurchaseDate = new Date(
3818
- cached.data.subscription.originalPurchaseDate
3819
- );
3820
- }
3821
- if (cached.data.subscription?.latestPurchaseDate) {
3822
- cached.data.subscription.latestPurchaseDate = new Date(
3823
- cached.data.subscription.latestPurchaseDate
3824
- );
3825
- }
3826
- if (cached.data.subscription?.cancellationDate) {
3827
- cached.data.subscription.cancellationDate = new Date(
3828
- cached.data.subscription.cancellationDate
3829
- );
3830
- }
3831
- if (cached.data.subscription?.gracePeriodExpiresAt) {
3832
- cached.data.subscription.gracePeriodExpiresAt = new Date(
3833
- cached.data.subscription.gracePeriodExpiresAt
3834
- );
3835
- }
3836
- cached.isStale = this.isExpired(cached);
3837
- if (!cached.isStale) {
3838
- this.memoryCache = cached;
3839
- }
3840
- return cached;
3841
- } catch {
3842
- return null;
3843
- }
3844
- }
3845
- async set(data) {
3846
- const cached = {
3847
- data,
3848
- cachedAt: Date.now(),
3849
- isStale: false
3850
- };
3851
- this.memoryCache = cached;
3852
- try {
3853
- await this.storage.set(CACHE_KEY, JSON.stringify(cached));
3854
- } catch {
3855
- }
3856
- }
3857
- async invalidate() {
3858
- this.memoryCache = null;
3859
- try {
3860
- await this.storage.remove(CACHE_KEY);
3861
- } catch {
3862
- }
3863
- }
3864
- isExpired(cached) {
3865
- return Date.now() - cached.cachedAt > this.ttl;
3866
- }
3867
- setTTL(ttl) {
3868
- this.ttl = ttl;
3869
- }
3870
- };
3871
-
3872
- // src/domains/subscription/SubscriptionManager.ts
3873
4087
  var SubscriptionManagerClass = class {
3874
4088
  constructor() {
3875
4089
  this.config = null;
@@ -4001,10 +4215,10 @@ var subscriptionManager = new SubscriptionManagerClass();
4001
4215
 
4002
4216
  // src/hooks/useSubscription.ts
4003
4217
  function useSubscription() {
4004
- const [status, setStatus] = useState7(null);
4005
- const [isLoading, setIsLoading] = useState7(true);
4006
- const [error, setError] = useState7(null);
4007
- const refresh = useCallback7(async () => {
4218
+ const [status, setStatus] = useState8(null);
4219
+ const [isLoading, setIsLoading] = useState8(true);
4220
+ const [error, setError] = useState8(null);
4221
+ const refresh = useCallback8(async () => {
4008
4222
  setIsLoading(true);
4009
4223
  setError(null);
4010
4224
  try {
@@ -4017,7 +4231,7 @@ function useSubscription() {
4017
4231
  setIsLoading(false);
4018
4232
  }
4019
4233
  }, []);
4020
- const restore = useCallback7(async () => {
4234
+ const restore = useCallback8(async () => {
4021
4235
  setIsLoading(true);
4022
4236
  setError(null);
4023
4237
  try {
@@ -4054,11 +4268,11 @@ function useSubscription() {
4054
4268
  }
4055
4269
 
4056
4270
  // src/PaywalloProvider.tsx
4057
- import { useCallback as useCallback12, useEffect as useEffect7, useMemo as useMemo2, useRef as useRef8, useState as useState11 } from "react";
4271
+ import { useCallback as useCallback13, useEffect as useEffect7, useMemo as useMemo2, useRef as useRef8, useState as useState12 } from "react";
4058
4272
  import { Animated as Animated3, Dimensions as Dimensions4, Easing as Easing2, StyleSheet as StyleSheet3 } from "react-native";
4059
4273
 
4060
4274
  // src/hooks/useCampaignPreload.ts
4061
- import { useCallback as useCallback8, useRef as useRef6, useState as useState8 } from "react";
4275
+ import { useCallback as useCallback9, useRef as useRef6, useState as useState9 } from "react";
4062
4276
 
4063
4277
  // src/utils/serverProduct.ts
4064
4278
  var PERIOD_MAP = {
@@ -4125,17 +4339,17 @@ function consumeCacheEntry(cache, placement) {
4125
4339
 
4126
4340
  // src/hooks/useCampaignPreload.ts
4127
4341
  function useCampaignPreload() {
4128
- const [preloadedWebView, setPreloadedWebView] = useState8(null);
4342
+ const [preloadedWebView, setPreloadedWebView] = useState9(null);
4129
4343
  const preloadedCampaignsRef = useRef6(/* @__PURE__ */ new Map());
4130
- const isPreloadValid = useCallback8(
4344
+ const isPreloadValid = useCallback9(
4131
4345
  (placement) => isCacheEntryValid(preloadedCampaignsRef.current.get(placement)),
4132
4346
  []
4133
4347
  );
4134
- const consumePreloadedCampaign = useCallback8(
4348
+ const consumePreloadedCampaign = useCallback9(
4135
4349
  (placement) => consumeCacheEntry(preloadedCampaignsRef.current, placement),
4136
4350
  []
4137
4351
  );
4138
- const preloadCampaign = useCallback8(
4352
+ const preloadCampaign = useCallback9(
4139
4353
  async (placement, context) => {
4140
4354
  try {
4141
4355
  const campaign = await PaywalloClient.getCampaign(placement, context);
@@ -4201,11 +4415,11 @@ function useCampaignPreload() {
4201
4415
  }
4202
4416
 
4203
4417
  // src/hooks/useProductLoader.ts
4204
- import { useCallback as useCallback9, useState as useState9 } from "react";
4418
+ import { useCallback as useCallback10, useState as useState10 } from "react";
4205
4419
  function useProductLoader() {
4206
- const [products, setProducts] = useState9(/* @__PURE__ */ new Map());
4207
- const [isLoadingProducts, setIsLoadingProducts] = useState9(false);
4208
- const refreshProducts = useCallback9(async (productIds) => {
4420
+ const [products, setProducts] = useState10(/* @__PURE__ */ new Map());
4421
+ const [isLoadingProducts, setIsLoadingProducts] = useState10(false);
4422
+ const refreshProducts = useCallback10(async (productIds) => {
4209
4423
  setIsLoadingProducts(true);
4210
4424
  try {
4211
4425
  const iapService = getIAPService();
@@ -4223,10 +4437,10 @@ function useProductLoader() {
4223
4437
  }
4224
4438
 
4225
4439
  // src/hooks/usePurchaseOrchestrator.ts
4226
- import { useCallback as useCallback10 } from "react";
4440
+ import { useCallback as useCallback11 } from "react";
4227
4441
  function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef) {
4228
4442
  const { trackDismissed, trackProductSelected, trackPurchased } = usePaywallTracking();
4229
- const handlePreloadedPurchase = useCallback10(
4443
+ const handlePreloadedPurchase = useCallback11(
4230
4444
  async (productId) => {
4231
4445
  try {
4232
4446
  if (preloadedWebView) {
@@ -4261,7 +4475,7 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4261
4475
  },
4262
4476
  [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, trackProductSelected, trackPurchased]
4263
4477
  );
4264
- const handlePreloadedRestore = useCallback10(async () => {
4478
+ const handlePreloadedRestore = useCallback11(async () => {
4265
4479
  try {
4266
4480
  const transactions = await getIAPService().restore();
4267
4481
  if (transactions.length === 0) return;
@@ -4283,16 +4497,58 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4283
4497
  }
4284
4498
 
4285
4499
  // src/hooks/useSubscriptionSync.ts
4286
- import { useCallback as useCallback11, useRef as useRef7, useState as useState10 } from "react";
4500
+ import { useCallback as useCallback12, useRef as useRef7, useState as useState11 } from "react";
4287
4501
  var CACHE_TTL_MS2 = 5 * 60 * 1e3;
4288
- var ACTIVE_STATUSES = ["active", "in_grace_period"];
4502
+ var ACTIVE_STATUSES = ["active", "grace_period"];
4503
+ function toDomainSubscriptionInfo(sub) {
4504
+ return {
4505
+ id: "",
4506
+ productId: sub.productId,
4507
+ // The hook's SubscriptionStatus is a subset of the domain's — cast is safe
4508
+ status: sub.status,
4509
+ expiresAt: sub.expiresAt ?? /* @__PURE__ */ new Date(0),
4510
+ originalPurchaseDate: /* @__PURE__ */ new Date(0),
4511
+ latestPurchaseDate: /* @__PURE__ */ new Date(0),
4512
+ willRenew: sub.autoRenewEnabled,
4513
+ isTrial: false,
4514
+ isIntroductoryPeriod: false,
4515
+ platform: sub.platform
4516
+ };
4517
+ }
4518
+ function toDomainResponse(status) {
4519
+ return {
4520
+ hasActiveSubscription: status.hasActiveSubscription,
4521
+ subscription: status.subscription ? toDomainSubscriptionInfo(status.subscription) : null,
4522
+ entitlements: []
4523
+ };
4524
+ }
4525
+ async function resolveOfflineFromCache() {
4526
+ try {
4527
+ const cached = await subscriptionCache.get();
4528
+ if (!cached) return false;
4529
+ const sub = cached.data.subscription;
4530
+ if (!sub) return false;
4531
+ if (!ACTIVE_STATUSES.includes(sub.status)) return false;
4532
+ const hasValidExpiry = sub.expiresAt.getTime() === 0 || sub.expiresAt > /* @__PURE__ */ new Date();
4533
+ return hasValidExpiry;
4534
+ } catch {
4535
+ return false;
4536
+ }
4537
+ }
4538
+ function isActiveCacheEntry(entry) {
4539
+ if (!entry.data) return false;
4540
+ if (!ACTIVE_STATUSES.includes(entry.data.status)) return false;
4541
+ if (entry.data.expiresAt && entry.data.expiresAt < /* @__PURE__ */ new Date()) return false;
4542
+ return true;
4543
+ }
4289
4544
  function useSubscriptionSync() {
4290
- const [subscription, setSubscription] = useState10(null);
4545
+ const [subscription, setSubscription] = useState11(null);
4291
4546
  const cacheRef = useRef7(null);
4292
- const invalidateCache = useCallback11(() => {
4547
+ const invalidateCache = useCallback12(() => {
4293
4548
  cacheRef.current = null;
4549
+ void subscriptionCache.invalidate();
4294
4550
  }, []);
4295
- const fetchAndCache = useCallback11(async () => {
4551
+ const fetchAndCache = useCallback12(async () => {
4296
4552
  const apiClient = PaywalloClient.getApiClient();
4297
4553
  const distinctId = PaywalloClient.getDistinctId();
4298
4554
  if (!apiClient || !distinctId) return null;
@@ -4303,18 +4559,37 @@ function useSubscriptionSync() {
4303
4559
  } : null;
4304
4560
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4305
4561
  setSubscription(sub);
4562
+ void subscriptionCache.set(toDomainResponse(status));
4306
4563
  return sub;
4307
4564
  }, []);
4308
- const hasActiveSubscription = useCallback11(async () => {
4565
+ const hasActiveSubscription = useCallback12(async () => {
4309
4566
  const apiClient = PaywalloClient.getApiClient();
4310
4567
  const distinctId = PaywalloClient.getDistinctId();
4311
4568
  if (!apiClient || !distinctId) return false;
4312
4569
  const cached = cacheRef.current;
4313
4570
  if (cached && cached.expiresAt > Date.now()) {
4314
- if (!cached.data) return false;
4315
- if (!ACTIVE_STATUSES.includes(cached.data.status)) return false;
4316
- if (cached.data.expiresAt && cached.data.expiresAt < /* @__PURE__ */ new Date()) return false;
4317
- return true;
4571
+ return isActiveCacheEntry(cached);
4572
+ }
4573
+ try {
4574
+ const persisted = await subscriptionCache.get();
4575
+ if (persisted) {
4576
+ if (!persisted.isStale) {
4577
+ const sub = persisted.data.subscription ? {
4578
+ productId: persisted.data.subscription.productId,
4579
+ status: persisted.data.subscription.status,
4580
+ expiresAt: persisted.data.subscription.expiresAt ?? null,
4581
+ platform: persisted.data.subscription.platform,
4582
+ autoRenewEnabled: persisted.data.subscription.willRenew,
4583
+ inGracePeriod: persisted.data.subscription.status === "grace_period"
4584
+ } : null;
4585
+ cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4586
+ setSubscription(sub);
4587
+ return persisted.data.hasActiveSubscription;
4588
+ }
4589
+ void fetchAndCache();
4590
+ return persisted.data.hasActiveSubscription;
4591
+ }
4592
+ } catch {
4318
4593
  }
4319
4594
  try {
4320
4595
  const status = await apiClient.getSubscriptionStatus(distinctId);
@@ -4324,12 +4599,13 @@ function useSubscriptionSync() {
4324
4599
  } : null;
4325
4600
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4326
4601
  setSubscription(sub);
4602
+ void subscriptionCache.set(toDomainResponse(status));
4327
4603
  return status.hasActiveSubscription;
4328
4604
  } catch {
4329
- return false;
4605
+ return await resolveOfflineFromCache();
4330
4606
  }
4331
- }, []);
4332
- const getSubscription = useCallback11(async () => {
4607
+ }, [fetchAndCache]);
4608
+ const getSubscription = useCallback12(async () => {
4333
4609
  const apiClient = PaywalloClient.getApiClient();
4334
4610
  const distinctId = PaywalloClient.getDistinctId();
4335
4611
  if (!apiClient || !distinctId) return null;
@@ -4338,7 +4614,22 @@ function useSubscriptionSync() {
4338
4614
  try {
4339
4615
  return await fetchAndCache();
4340
4616
  } catch {
4341
- return null;
4617
+ try {
4618
+ const persisted = await subscriptionCache.get();
4619
+ if (!persisted?.data.subscription) return null;
4620
+ const s = persisted.data.subscription;
4621
+ const normalisedStatus = s.status === "grace_period" ? "in_grace_period" : s.status;
4622
+ return {
4623
+ productId: s.productId,
4624
+ status: normalisedStatus,
4625
+ expiresAt: s.expiresAt ?? null,
4626
+ platform: s.platform,
4627
+ autoRenewEnabled: s.willRenew,
4628
+ inGracePeriod: normalisedStatus === "in_grace_period"
4629
+ };
4630
+ } catch {
4631
+ return null;
4632
+ }
4342
4633
  }
4343
4634
  }, [fetchAndCache]);
4344
4635
  return { subscription, hasActiveSubscription, getSubscription, invalidateCache };
@@ -4347,14 +4638,14 @@ function useSubscriptionSync() {
4347
4638
  // src/PaywalloProvider.tsx
4348
4639
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
4349
4640
  function PaywalloProvider({ children, config }) {
4350
- const [isInitialized, setIsInitialized] = useState11(false);
4351
- const [initError, setInitError] = useState11(null);
4352
- const [distinctId, setDistinctId] = useState11(null);
4353
- const [activePaywall, setActivePaywall] = useState11(null);
4641
+ const [isInitialized, setIsInitialized] = useState12(false);
4642
+ const [initError, setInitError] = useState12(null);
4643
+ const [distinctId, setDistinctId] = useState12(null);
4644
+ const [activePaywall, setActivePaywall] = useState12(null);
4354
4645
  const { products, isLoadingProducts, refreshProducts } = useProductLoader();
4355
4646
  const { subscription, hasActiveSubscription, getSubscription, invalidateCache } = useSubscriptionSync();
4356
4647
  const { preloadedWebView, setPreloadedWebView, preloadCampaign, consumePreloadedCampaign, isPreloadValid } = useCampaignPreload();
4357
- const clearPreloadedWebView = useCallback12(() => setPreloadedWebView(null), [setPreloadedWebView]);
4648
+ const clearPreloadedWebView = useCallback13(() => setPreloadedWebView(null), [setPreloadedWebView]);
4358
4649
  const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady, handlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreloadedWebView);
4359
4650
  const { handlePreloadedPurchase: rawPreloadedPurchase, handlePreloadedRestore } = usePurchaseOrchestrator(
4360
4651
  preloadedWebView,
@@ -4363,8 +4654,8 @@ function PaywalloProvider({ children, config }) {
4363
4654
  setShowingPreloadedWebView,
4364
4655
  presentedAtRef
4365
4656
  );
4366
- const [isPreloadPurchasing, setIsPreloadPurchasing] = useState11(false);
4367
- const handlePreloadedPurchase = useCallback12(async (productId) => {
4657
+ const [isPreloadPurchasing, setIsPreloadPurchasing] = useState12(false);
4658
+ const handlePreloadedPurchase = useCallback13(async (productId) => {
4368
4659
  setIsPreloadPurchasing(true);
4369
4660
  try {
4370
4661
  await rawPreloadedPurchase(productId);
@@ -4381,21 +4672,21 @@ function PaywalloProvider({ children, config }) {
4381
4672
  setInitError(err instanceof Error ? err : new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, String(err)));
4382
4673
  });
4383
4674
  }, [config]);
4384
- const getPaywallConfig = useCallback12(async (p) => {
4675
+ const getPaywallConfig = useCallback13(async (p) => {
4385
4676
  const api = PaywalloClient.getApiClient();
4386
4677
  return api ? api.getPaywall(p) : null;
4387
4678
  }, []);
4388
- const presentPaywall = useCallback12((placement) => {
4679
+ const presentPaywall = useCallback13((placement) => {
4389
4680
  return new Promise((resolve) => setActivePaywall({ placement, resolver: resolve }));
4390
4681
  }, []);
4391
- const trackCampaignImpression = useCallback12((placement, variantKey, campaignId) => {
4682
+ const trackCampaignImpression = useCallback13((placement, variantKey, campaignId) => {
4392
4683
  const api = PaywalloClient.getApiClient();
4393
4684
  if (!api) return;
4394
4685
  const sessionId = PaywalloClient.getSessionId();
4395
4686
  void api.trackEvent("$campaign_impression", PaywalloClient.getDistinctId(), { placement, variantKey, campaignId, ...sessionId && { sessionId } }).catch(() => {
4396
4687
  });
4397
4688
  }, []);
4398
- const presentCampaign = useCallback12(
4689
+ const presentCampaign = useCallback13(
4399
4690
  async (placement, context) => {
4400
4691
  try {
4401
4692
  if (preloadedWebView?.placement === placement && preloadedWebView.loaded && isPreloadValid(placement)) {
@@ -4464,7 +4755,7 @@ function PaywalloProvider({ children, config }) {
4464
4755
  },
4465
4756
  [preloadedWebView, isPreloadValid, consumePreloadedCampaign, presentPreloadedWebView, trackCampaignImpression]
4466
4757
  );
4467
- const restorePurchases = useCallback12(async () => {
4758
+ const restorePurchases = useCallback13(async () => {
4468
4759
  invalidateCache();
4469
4760
  try {
4470
4761
  const txs = await getIAPService().restore();
@@ -4473,7 +4764,7 @@ function PaywalloProvider({ children, config }) {
4473
4764
  return { success: false, restoredProducts: [], error: error instanceof Error ? error : new PurchaseError(PURCHASE_ERROR_CODES.RESTORE_FAILED, String(error)) };
4474
4765
  }
4475
4766
  }, [invalidateCache]);
4476
- const handlePaywallResult = useCallback12((result) => {
4767
+ const handlePaywallResult = useCallback13((result) => {
4477
4768
  if (activePaywall) {
4478
4769
  activePaywall.resolver(result);
4479
4770
  setActivePaywall(null);
@@ -4496,7 +4787,7 @@ function PaywalloProvider({ children, config }) {
4496
4787
  useEffect7(() => {
4497
4788
  if (activePaywall) setPreloadedWebView(null);
4498
4789
  }, [activePaywall, setPreloadedWebView]);
4499
- const noOp = useCallback12(() => {
4790
+ const noOp = useCallback13(() => {
4500
4791
  }, []);
4501
4792
  const contextValue = useMemo2(() => ({
4502
4793
  config: PaywalloClient.getConfig(),