@virex-tech/paywallo-sdk 1.1.0 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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";
@@ -946,6 +955,13 @@ function getWebViewEmitter() {
946
955
  }
947
956
  return webViewEmitter;
948
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
+ }
949
965
  function PaywallWebView({
950
966
  paywallId,
951
967
  craftData,
@@ -956,11 +972,14 @@ function PaywallWebView({
956
972
  onPurchase,
957
973
  onClose,
958
974
  onRestore,
959
- onReady
975
+ onReady,
976
+ onError
960
977
  }) {
961
978
  const paywallDataSent = useRef3(false);
962
979
  const loadEndReceived = useRef3(false);
963
980
  const [scriptToInject, setScriptToInject] = useState3(void 0);
981
+ const onErrorRef = useRef3(onError);
982
+ onErrorRef.current = onError;
964
983
  const NativeWebView = useMemo(() => getNativeWebView(), []);
965
984
  const webUrl = useMemo(() => PaywalloClient.getWebUrl() ?? "http://localhost:3000", []);
966
985
  const buildAndSetInjectionScript = useCallback3(() => {
@@ -1020,17 +1039,51 @@ function PaywallWebView({
1020
1039
  const message = parseWebViewMessage(event.data);
1021
1040
  if (message) handleParsedMessage(message);
1022
1041
  });
1042
+ const errorSub = emitter.addListener(
1043
+ "PaywalloWebViewError",
1044
+ (event) => {
1045
+ onErrorRef.current?.(parseErrorEvent(event));
1046
+ }
1047
+ );
1023
1048
  return () => {
1024
1049
  loadEndSub.remove();
1025
1050
  messageSub.remove();
1051
+ errorSub.remove();
1026
1052
  };
1027
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
+ }, []);
1028
1065
  const handleLoadEnd = useCallback3(() => {
1029
1066
  if (!loadEndReceived.current) {
1030
1067
  loadEndReceived.current = true;
1031
1068
  buildAndSetInjectionScript();
1032
1069
  }
1033
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
+ }, []);
1034
1087
  const handleMessage = useCallback3(
1035
1088
  (event) => {
1036
1089
  const message = parseWebViewMessage(event.nativeEvent.data);
@@ -1050,6 +1103,7 @@ function PaywallWebView({
1050
1103
  injectedJavaScript: scriptToInject,
1051
1104
  onMessage: handleMessage,
1052
1105
  onLoadEnd: handleLoadEnd,
1106
+ onError: handleNativeError,
1053
1107
  style: styles.webview
1054
1108
  }
1055
1109
  ) });
@@ -1060,7 +1114,7 @@ var styles = StyleSheet.create({
1060
1114
  });
1061
1115
 
1062
1116
  // src/domains/paywall/PaywallModal.tsx
1063
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1117
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
1064
1118
  var SCREEN_HEIGHT2 = Dimensions2.get("window").height;
1065
1119
  function PaywallModal({
1066
1120
  placement,
@@ -1080,6 +1134,25 @@ function PaywallModal({
1080
1134
  campaignId
1081
1135
  );
1082
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
+ }, []);
1083
1156
  const openAnim = useRef4(new Animated2.Value(SCREEN_HEIGHT2)).current;
1084
1157
  useEffect3(() => {
1085
1158
  if (visible) {
@@ -1104,7 +1177,12 @@ function PaywallModal({
1104
1177
  statusBarTranslucent: true,
1105
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: [
1106
1179
  error && /* @__PURE__ */ jsx2(View2, { style: styles2.errorContainer, children: /* @__PURE__ */ jsx2(Text, { style: styles2.errorText, children: error.message }) }),
1107
- !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(
1108
1186
  PaywallWebView,
1109
1187
  {
1110
1188
  paywallId: paywallConfig.id,
@@ -1115,39 +1193,67 @@ function PaywallModal({
1115
1193
  isPurchasing,
1116
1194
  onClose: handleClose,
1117
1195
  onPurchase: handlePurchase,
1118
- onRestore: handleRestore
1119
- }
1120
- )
1196
+ onRestore: handleRestore,
1197
+ onError: handleWebViewError
1198
+ },
1199
+ retryCount
1200
+ ) })
1121
1201
  ] }) }) })
1122
1202
  }
1123
1203
  );
1124
1204
  }
1125
1205
  var styles2 = StyleSheet2.create({
1126
- 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: {
1127
1212
  flex: 1,
1128
- backgroundColor: "rgba(0,0,0,0.3)"
1213
+ justifyContent: "center",
1214
+ alignItems: "center",
1215
+ backgroundColor: "#111010",
1216
+ padding: 24
1129
1217
  },
1130
- animatedContainer: {
1131
- flex: 1,
1132
- backgroundColor: "#fff",
1133
- borderTopLeftRadius: 0,
1134
- 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
1135
1225
  },
1136
- container: {
1137
- flex: 1,
1138
- backgroundColor: "#fff"
1226
+ fallbackTitle: {
1227
+ fontSize: 18,
1228
+ fontWeight: "600",
1229
+ color: "#f5f5f5",
1230
+ marginBottom: 10,
1231
+ textAlign: "center"
1139
1232
  },
1140
- errorContainer: {
1141
- flex: 1,
1142
- 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,
1143
1245
  alignItems: "center",
1144
- padding: 20
1246
+ marginBottom: 12
1145
1247
  },
1146
- errorText: {
1147
- fontSize: 16,
1148
- color: "#ef4444",
1149
- textAlign: "center"
1150
- }
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)" }
1151
1257
  });
1152
1258
 
1153
1259
  // src/domains/paywall/PaywallErrorBoundary.tsx
@@ -1183,9 +1289,9 @@ function usePaywallContext() {
1183
1289
  }
1184
1290
 
1185
1291
  // src/domains/paywall/usePaywallPresenter.ts
1186
- 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";
1187
1293
  function usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreloadedWebView) {
1188
- const [showingPreloadedWebView, setShowingPreloadedWebView] = useState4(false);
1294
+ const [showingPreloadedWebView, setShowingPreloadedWebView] = useState5(false);
1189
1295
  const resolverRef = useRef5(null);
1190
1296
  const presentedAtRef = useRef5(Date.now());
1191
1297
  useEffect4(() => {
@@ -1196,23 +1302,23 @@ function usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreload
1196
1302
  }
1197
1303
  };
1198
1304
  }, []);
1199
- const presentPreloadedWebView = useCallback4(() => {
1305
+ const presentPreloadedWebView = useCallback5(() => {
1200
1306
  return new Promise((resolve) => {
1201
1307
  resolverRef.current = resolve;
1202
1308
  presentedAtRef.current = Date.now();
1203
1309
  setShowingPreloadedWebView(true);
1204
1310
  });
1205
1311
  }, []);
1206
- const resolvePreloadedPaywall = useCallback4((result) => {
1312
+ const resolvePreloadedPaywall = useCallback5((result) => {
1207
1313
  if (resolverRef.current) {
1208
1314
  resolverRef.current(result);
1209
1315
  resolverRef.current = null;
1210
1316
  }
1211
1317
  }, []);
1212
- const handlePreloadedWebViewReady = useCallback4(() => {
1318
+ const handlePreloadedWebViewReady = useCallback5(() => {
1213
1319
  setPreloadedWebView((prev) => prev ? { ...prev, loaded: true } : null);
1214
1320
  }, [setPreloadedWebView]);
1215
- const handlePreloadedClose = useCallback4(() => {
1321
+ const handlePreloadedClose = useCallback5(() => {
1216
1322
  const track = async () => {
1217
1323
  try {
1218
1324
  const apiClient = PaywalloClient.getApiClient();
@@ -1462,6 +1568,92 @@ var PaywalloAffiliate = {
1462
1568
  getWithdrawals: () => affiliateManager.getWithdrawals()
1463
1569
  };
1464
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
+
1465
1657
  // src/core/ApiClient.ts
1466
1658
  import { Platform as Platform3 } from "react-native";
1467
1659
 
@@ -2324,7 +2516,7 @@ var ApiCache = class {
2324
2516
  // src/core/ApiClient.ts
2325
2517
  var SDK_VERSION = "1.0.0";
2326
2518
  var DEFAULT_BASE_URL = "https://panel.lucasqueiroga.shop";
2327
- var ApiClient = class {
2519
+ var _ApiClient = class _ApiClient {
2328
2520
  constructor(appKey, debug = false, environment = "Production", offlineQueueEnabled = true, timeout = 3e4) {
2329
2521
  this.cache = new ApiCache();
2330
2522
  this.appKey = appKey;
@@ -2414,6 +2606,11 @@ var ApiClient = class {
2414
2606
  const key = `${flagKey}:${distinctId}`;
2415
2607
  const hit = this.cache.getVariant(key);
2416
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
+ }
2417
2614
  try {
2418
2615
  const res = await this.get(`/api/v1/flags/${flagKey}?distinctId=${encodeURIComponent(distinctId)}`);
2419
2616
  if (res.status === 404) {
@@ -2428,6 +2625,7 @@ var ApiClient = class {
2428
2625
  return { variant: null };
2429
2626
  }
2430
2627
  this.cache.setVariant(key, res.data);
2628
+ await this.writeFlagToStorage(flagKey, distinctId, res.data);
2431
2629
  return res.data;
2432
2630
  } catch (error) {
2433
2631
  this.log("Failed to get variant:", flagKey, error);
@@ -2509,6 +2707,35 @@ var ApiClient = class {
2509
2707
  this.log("Subscription status:", result.hasActiveSubscription);
2510
2708
  return result;
2511
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
+ }
2512
2739
  async getConditionalFlag(flagKey, context) {
2513
2740
  try {
2514
2741
  const url = buildConditionalFlagUrl(this.baseUrl, flagKey, context);
@@ -2525,7 +2752,43 @@ var ApiClient = class {
2525
2752
  return { value: false, flagKey };
2526
2753
  }
2527
2754
  }
2528
- // ─── 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
+ }
2529
2792
  /**
2530
2793
  * POST with offline queue fallback. Enqueues when offline or on network failure.
2531
2794
  * Throws only when offlineQueueEnabled is false and the request fails.
@@ -2552,6 +2815,9 @@ var ApiClient = class {
2552
2815
  if (this.debug) console.warn("[Paywallo API]", ...args);
2553
2816
  }
2554
2817
  };
2818
+ // ─── Private helpers ─────────────────────────────────────────────────────────
2819
+ _ApiClient.FLAG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
2820
+ var ApiClient = _ApiClient;
2555
2821
 
2556
2822
  // src/domains/campaign/CampaignError.ts
2557
2823
  var CampaignError = class extends PaywalloError {
@@ -2579,18 +2845,21 @@ var CampaignGateService = class {
2579
2845
  if (!distinctId) return null;
2580
2846
  return apiClient.getCampaign(placement, distinctId, context);
2581
2847
  }
2582
- async presentCampaign(apiClient, placement, paywallPresenter, campaignPresenter, context) {
2848
+ async presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
2583
2849
  const campaign = await this.getCampaign(apiClient, placement, context);
2584
2850
  if (!campaign) {
2585
2851
  return { presented: false, purchased: false, cancelled: false, restored: false, error: new CampaignError(CAMPAIGN_ERROR_CODES.NOT_FOUND, "No campaign found") };
2586
2852
  }
2853
+ if (!context?.forceShow && await hasActive()) {
2854
+ return { presented: false, purchased: false, cancelled: false, restored: false, skippedReason: "subscriber" };
2855
+ }
2587
2856
  if (campaignPresenter) return campaignPresenter(campaign);
2588
2857
  if (!paywallPresenter) throw new PaywallError(PAYWALL_ERROR_CODES.NOT_INITIALIZED, "PaywalloProvider not mounted. Wrap your app with <PaywalloProvider>");
2589
2858
  return { ...await paywallPresenter(campaign.paywall.placement), campaignId: campaign.campaignId, variantKey: campaign.variantKey };
2590
2859
  }
2591
2860
  async requireSubscriptionWithCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context) {
2592
2861
  if (await hasActive()) return true;
2593
- const r = await this.presentCampaign(apiClient, placement, paywallPresenter, campaignPresenter, context);
2862
+ const r = await this.presentCampaign(apiClient, placement, hasActive, paywallPresenter, campaignPresenter, context);
2594
2863
  return r.purchased || r.restored;
2595
2864
  }
2596
2865
  async preloadCampaign(apiClient, placement, context) {
@@ -3127,6 +3396,20 @@ function isValidEventName(name) {
3127
3396
  if (name.startsWith("$")) return /^\$[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
3128
3397
  return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
3129
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
+ }
3130
3413
  var _PaywalloClientClass = class _PaywalloClientClass {
3131
3414
  constructor() {
3132
3415
  this.config = null;
@@ -3143,27 +3426,27 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3143
3426
  this.initAttempts = 0;
3144
3427
  }
3145
3428
  async init(config) {
3146
- 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");
3147
3430
  if (this.config && this.apiClient) {
3148
- console.warn("[Paywallo INIT]", "Already initialized \u2014 skipping");
3431
+ if (config.debug) console.warn("[Paywallo INIT]", "Already initialized \u2014 skipping");
3149
3432
  return;
3150
3433
  }
3151
3434
  if (this.initPromise) {
3152
- console.warn("[Paywallo INIT]", "Init already in progress \u2014 awaiting...");
3435
+ if (config.debug) console.warn("[Paywallo INIT]", "Init already in progress \u2014 awaiting...");
3153
3436
  await this.initPromise;
3154
- console.warn("[Paywallo INIT]", "Awaited existing init \u2014 done");
3437
+ if (config.debug) console.warn("[Paywallo INIT]", "Awaited existing init \u2014 done");
3155
3438
  return;
3156
3439
  }
3157
3440
  if (!config.appKey) throw new ClientError(CLIENT_ERROR_CODES.MISSING_APP_KEY, "appKey is required");
3158
- console.warn("[Paywallo INIT]", "Starting fresh init...");
3441
+ if (config.debug) console.warn("[Paywallo INIT]", "Starting fresh init...");
3159
3442
  this.pendingConfig = config;
3160
3443
  this.initAttempts = 0;
3161
3444
  this.initPromise = this.initWithRetry(config);
3162
3445
  try {
3163
3446
  await this.initPromise;
3164
- console.warn("[Paywallo INIT]", "init() SUCCESS");
3447
+ if (config.debug) console.warn("[Paywallo INIT]", "init() SUCCESS");
3165
3448
  } catch (error) {
3166
- 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));
3167
3450
  this.initPromise = null;
3168
3451
  this.config = null;
3169
3452
  this.apiClient = null;
@@ -3233,26 +3516,26 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3233
3516
  async doInit(config) {
3234
3517
  const environment = config.environment ?? (typeof __DEV__ !== "undefined" && __DEV__ ? "Sandbox" : "Production");
3235
3518
  const offlineQueueEnabled = config.offlineQueueEnabled !== false;
3236
- console.warn("[Paywallo INIT]", "Step 1: networkMonitor + offlineQueue...");
3519
+ if (config.debug) console.warn("[Paywallo INIT]", "Step 1: networkMonitor + offlineQueue...");
3237
3520
  await Promise.all([
3238
3521
  networkMonitor.initialize({ debug: config.debug }),
3239
3522
  offlineQueue.initialize({ debug: config.debug }).then(
3240
3523
  () => offlineQueue.clearItemsWithInvalidAppKey(config.appKey)
3241
3524
  )
3242
3525
  ]);
3243
- console.warn("[Paywallo INIT]", "Step 1 DONE");
3244
- 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...");
3245
3528
  this.apiClient = new ApiClient(config.appKey, config.debug, environment, offlineQueueEnabled, config.timeout);
3246
3529
  queueProcessor.initialize(this.apiClient.getHttpClient(), { debug: config.debug });
3247
3530
  affiliateManager.initialize(this.apiClient, config.debug);
3248
- console.warn("[Paywallo INIT]", "Step 2 DONE");
3249
- 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...");
3250
3533
  await Promise.all([
3251
3534
  identityManager.initialize(this.apiClient, config.debug),
3252
3535
  sessionManager.initialize(this.apiClient, config.sessionConfig, config.debug)
3253
3536
  ]);
3254
3537
  const distinctId = identityManager.getDistinctId();
3255
- 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");
3256
3539
  if (config.autoStartSession !== false) {
3257
3540
  sessionManager.startSession().catch(() => {
3258
3541
  if (config.debug) console.warn("[Paywallo]", "Session start failed during init \u2014 continuing");
@@ -3261,7 +3544,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3261
3544
  new InstallTracker(config.debug).trackIfNeeded(this.apiClient, config.requestATT === true).catch(() => {
3262
3545
  });
3263
3546
  this.config = config;
3264
- console.warn("[Paywallo INIT]", "COMPLETE \u2014 SDK ready, environment:", environment);
3547
+ if (config.debug) console.warn("[Paywallo INIT]", "COMPLETE \u2014 SDK ready, environment:", environment);
3265
3548
  }
3266
3549
  async identify(options) {
3267
3550
  this.ensureInitialized();
@@ -3271,7 +3554,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3271
3554
  const apiClient = this.getApiClientOrThrow();
3272
3555
  const distinctId = identityManager.getDistinctId();
3273
3556
  if (!distinctId) {
3274
- console.warn("[Paywallo TRACK]", "SKIPPED \u2014 no distinctId", eventName);
3557
+ if (this.config?.debug) console.warn("[Paywallo TRACK]", "SKIPPED \u2014 no distinctId", eventName);
3275
3558
  return;
3276
3559
  }
3277
3560
  if (!isValidEventName(eventName)) {
@@ -3280,7 +3563,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3280
3563
  }
3281
3564
  const sessionId = sessionManager.getSessionId();
3282
3565
  if (eventName === "$purchase_completed") {
3283
- console.warn("[Paywallo PURCHASE]", "tracking purchase:", JSON.stringify({
3566
+ if (this.config?.debug) console.warn("[Paywallo PURCHASE]", "tracking purchase:", JSON.stringify({
3284
3567
  distinctId: distinctId.substring(0, 8) + "...",
3285
3568
  productId: options?.properties?.productId,
3286
3569
  priceUsd: options?.properties?.priceUsd,
@@ -3289,14 +3572,14 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3289
3572
  placement: options?.properties?.placement
3290
3573
  }));
3291
3574
  } else if (eventName === "$paywall_viewed" || eventName === "$paywall_closed") {
3292
- console.warn("[Paywallo PAYWALL]", eventName, JSON.stringify({
3575
+ if (this.config?.debug) console.warn("[Paywallo PAYWALL]", eventName, JSON.stringify({
3293
3576
  distinctId: distinctId.substring(0, 8) + "...",
3294
3577
  placement: options?.properties?.placement,
3295
3578
  variantKey: options?.properties?.variantKey,
3296
3579
  timeOnPaywall: options?.properties?.timeOnPaywall
3297
3580
  }));
3298
3581
  } else {
3299
- 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) + "...");
3300
3583
  }
3301
3584
  await apiClient.trackEvent(eventName, distinctId, {
3302
3585
  ...identityManager.getProperties(),
@@ -3347,15 +3630,25 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3347
3630
  ...sessionId && { sessionId }
3348
3631
  });
3349
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
+ }
3350
3643
  async getVariant(flagKey) {
3351
3644
  try {
3352
3645
  const distinctId = identityManager.getDistinctId();
3353
3646
  if (!distinctId) {
3354
- 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);
3355
3648
  return { variant: null };
3356
3649
  }
3357
3650
  const result = await this.getApiClientOrThrow().getVariant(flagKey, distinctId);
3358
- console.warn("[Paywallo AB]", `getVariant("${flagKey}") =`, result.variant);
3651
+ if (this.config?.debug) console.warn("[Paywallo AB]", `getVariant("${flagKey}") =`, result.variant);
3359
3652
  return result;
3360
3653
  } catch (error) {
3361
3654
  if (this.config?.debug) console.warn("[Paywallo]", "getVariant failed \u2014 returning null", error);
@@ -3363,6 +3656,10 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3363
3656
  return { variant: null };
3364
3657
  }
3365
3658
  }
3659
+ async evaluateFlags(keys) {
3660
+ this.ensureInitialized();
3661
+ return this.getApiClientOrThrow().evaluateFlags(keys, identityManager.getDistinctId());
3662
+ }
3366
3663
  async getConditionalFlag(flagKey, context) {
3367
3664
  try {
3368
3665
  const distinctId = identityManager.getDistinctId();
@@ -3388,7 +3685,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3388
3685
  }
3389
3686
  async presentCampaign(placement, context) {
3390
3687
  this.ensureInitialized();
3391
- 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);
3392
3689
  }
3393
3690
  async hasActiveSubscription() {
3394
3691
  this.ensureInitialized();
@@ -3396,11 +3693,12 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3396
3693
  if (this.activeChecker) return await this.activeChecker();
3397
3694
  const distinctId = identityManager.getDistinctId();
3398
3695
  if (!distinctId || !this.apiClient) return false;
3399
- return (await this.apiClient.getSubscriptionStatus(distinctId)).hasActiveSubscription;
3696
+ const status = await this.apiClient.getSubscriptionStatus(distinctId);
3697
+ return status.hasActiveSubscription;
3400
3698
  } catch {
3401
- 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");
3402
3700
  if (this.apiClient) void this.apiClient.reportError("SUBSCRIPTION_CHECK_FAILED", "hasActiveSubscription failed");
3403
- return false;
3701
+ return resolveClientOfflineFromCache();
3404
3702
  }
3405
3703
  }
3406
3704
  async getSubscription() {
@@ -3429,6 +3727,7 @@ var _PaywalloClientClass = class _PaywalloClientClass {
3429
3727
  const wasDebug = this.config?.debug ?? false;
3430
3728
  await sessionManager.endSession();
3431
3729
  await identityManager.reset();
3730
+ await subscriptionCache.invalidate();
3432
3731
  queueProcessor.dispose();
3433
3732
  offlineQueue.dispose();
3434
3733
  networkMonitor.dispose();
@@ -3616,7 +3915,7 @@ function usePaywallo() {
3616
3915
  }
3617
3916
 
3618
3917
  // src/hooks/useProducts.ts
3619
- 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";
3620
3919
 
3621
3920
  // src/utils/productMappers.ts
3622
3921
  function mapToIAPProduct(product) {
@@ -3653,11 +3952,11 @@ function mapToFormattedProduct(product) {
3653
3952
 
3654
3953
  // src/hooks/useProducts.ts
3655
3954
  function useProducts(initialProductIds) {
3656
- const [products, setProducts] = useState5([]);
3657
- const [formattedProducts, setFormattedProducts] = useState5([]);
3658
- const [isLoading, setIsLoading] = useState5(false);
3659
- const [error, setError] = useState5(null);
3660
- 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) => {
3661
3960
  setIsLoading(true);
3662
3961
  setError(null);
3663
3962
  try {
@@ -3676,19 +3975,19 @@ function useProducts(initialProductIds) {
3676
3975
  void loadProducts(initialProductIds);
3677
3976
  }
3678
3977
  }, []);
3679
- const getProduct = useCallback5(
3978
+ const getProduct = useCallback6(
3680
3979
  (productId) => {
3681
3980
  return products.find((p) => p.productId === productId);
3682
3981
  },
3683
3982
  [products]
3684
3983
  );
3685
- const getFormattedProduct = useCallback5(
3984
+ const getFormattedProduct = useCallback6(
3686
3985
  (productId) => {
3687
3986
  return formattedProducts.find((p) => p.productId === productId);
3688
3987
  },
3689
3988
  [formattedProducts]
3690
3989
  );
3691
- const getPriceInfo = useCallback5(
3990
+ const getPriceInfo = useCallback6(
3692
3991
  (productId) => {
3693
3992
  const p = formattedProducts.find((fp) => fp.productId === productId);
3694
3993
  if (!p) return void 0;
@@ -3717,11 +4016,11 @@ function useProducts(initialProductIds) {
3717
4016
  }
3718
4017
 
3719
4018
  // src/hooks/usePurchase.ts
3720
- import { useCallback as useCallback6, useState as useState6 } from "react";
4019
+ import { useCallback as useCallback7, useState as useState7 } from "react";
3721
4020
  function usePurchase() {
3722
- const [state, setState] = useState6("idle");
3723
- const [error, setError] = useState6(null);
3724
- const purchase = useCallback6(async (productId) => {
4021
+ const [state, setState] = useState7("idle");
4022
+ const [error, setError] = useState7(null);
4023
+ const purchase = useCallback7(async (productId) => {
3725
4024
  setState("purchasing");
3726
4025
  setError(null);
3727
4026
  try {
@@ -3749,7 +4048,7 @@ function usePurchase() {
3749
4048
  throw err;
3750
4049
  }
3751
4050
  }, []);
3752
- const restore = useCallback6(async () => {
4051
+ const restore = useCallback7(async () => {
3753
4052
  setState("restoring");
3754
4053
  setError(null);
3755
4054
  try {
@@ -3781,90 +4080,10 @@ function usePurchase() {
3781
4080
  }
3782
4081
 
3783
4082
  // src/hooks/useSubscription.ts
3784
- 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";
3785
4084
 
3786
4085
  // src/domains/subscription/SubscriptionManager.ts
3787
4086
  import { Platform as Platform8 } from "react-native";
3788
-
3789
- // src/domains/subscription/SubscriptionCache.ts
3790
- var CACHE_KEY = "subscription_cache";
3791
- var DEFAULT_TTL = 5 * 60 * 1e3;
3792
- var SubscriptionCache = class {
3793
- constructor(ttl = DEFAULT_TTL) {
3794
- this.memoryCache = null;
3795
- this.ttl = ttl;
3796
- this.storage = new SecureStorage();
3797
- }
3798
- async get() {
3799
- if (this.memoryCache && !this.isExpired(this.memoryCache)) {
3800
- return this.memoryCache;
3801
- }
3802
- try {
3803
- const stored = await this.storage.get(CACHE_KEY);
3804
- if (!stored) {
3805
- return null;
3806
- }
3807
- const cached = JSON.parse(stored);
3808
- if (cached.data.subscription?.expiresAt) {
3809
- cached.data.subscription.expiresAt = new Date(cached.data.subscription.expiresAt);
3810
- }
3811
- if (cached.data.subscription?.originalPurchaseDate) {
3812
- cached.data.subscription.originalPurchaseDate = new Date(
3813
- cached.data.subscription.originalPurchaseDate
3814
- );
3815
- }
3816
- if (cached.data.subscription?.latestPurchaseDate) {
3817
- cached.data.subscription.latestPurchaseDate = new Date(
3818
- cached.data.subscription.latestPurchaseDate
3819
- );
3820
- }
3821
- if (cached.data.subscription?.cancellationDate) {
3822
- cached.data.subscription.cancellationDate = new Date(
3823
- cached.data.subscription.cancellationDate
3824
- );
3825
- }
3826
- if (cached.data.subscription?.gracePeriodExpiresAt) {
3827
- cached.data.subscription.gracePeriodExpiresAt = new Date(
3828
- cached.data.subscription.gracePeriodExpiresAt
3829
- );
3830
- }
3831
- cached.isStale = this.isExpired(cached);
3832
- if (!cached.isStale) {
3833
- this.memoryCache = cached;
3834
- }
3835
- return cached;
3836
- } catch {
3837
- return null;
3838
- }
3839
- }
3840
- async set(data) {
3841
- const cached = {
3842
- data,
3843
- cachedAt: Date.now(),
3844
- isStale: false
3845
- };
3846
- this.memoryCache = cached;
3847
- try {
3848
- await this.storage.set(CACHE_KEY, JSON.stringify(cached));
3849
- } catch {
3850
- }
3851
- }
3852
- async invalidate() {
3853
- this.memoryCache = null;
3854
- try {
3855
- await this.storage.remove(CACHE_KEY);
3856
- } catch {
3857
- }
3858
- }
3859
- isExpired(cached) {
3860
- return Date.now() - cached.cachedAt > this.ttl;
3861
- }
3862
- setTTL(ttl) {
3863
- this.ttl = ttl;
3864
- }
3865
- };
3866
-
3867
- // src/domains/subscription/SubscriptionManager.ts
3868
4087
  var SubscriptionManagerClass = class {
3869
4088
  constructor() {
3870
4089
  this.config = null;
@@ -3996,10 +4215,10 @@ var subscriptionManager = new SubscriptionManagerClass();
3996
4215
 
3997
4216
  // src/hooks/useSubscription.ts
3998
4217
  function useSubscription() {
3999
- const [status, setStatus] = useState7(null);
4000
- const [isLoading, setIsLoading] = useState7(true);
4001
- const [error, setError] = useState7(null);
4002
- 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 () => {
4003
4222
  setIsLoading(true);
4004
4223
  setError(null);
4005
4224
  try {
@@ -4012,7 +4231,7 @@ function useSubscription() {
4012
4231
  setIsLoading(false);
4013
4232
  }
4014
4233
  }, []);
4015
- const restore = useCallback7(async () => {
4234
+ const restore = useCallback8(async () => {
4016
4235
  setIsLoading(true);
4017
4236
  setError(null);
4018
4237
  try {
@@ -4049,11 +4268,11 @@ function useSubscription() {
4049
4268
  }
4050
4269
 
4051
4270
  // src/PaywalloProvider.tsx
4052
- 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";
4053
4272
  import { Animated as Animated3, Dimensions as Dimensions4, Easing as Easing2, StyleSheet as StyleSheet3 } from "react-native";
4054
4273
 
4055
4274
  // src/hooks/useCampaignPreload.ts
4056
- 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";
4057
4276
 
4058
4277
  // src/utils/serverProduct.ts
4059
4278
  var PERIOD_MAP = {
@@ -4120,17 +4339,17 @@ function consumeCacheEntry(cache, placement) {
4120
4339
 
4121
4340
  // src/hooks/useCampaignPreload.ts
4122
4341
  function useCampaignPreload() {
4123
- const [preloadedWebView, setPreloadedWebView] = useState8(null);
4342
+ const [preloadedWebView, setPreloadedWebView] = useState9(null);
4124
4343
  const preloadedCampaignsRef = useRef6(/* @__PURE__ */ new Map());
4125
- const isPreloadValid = useCallback8(
4344
+ const isPreloadValid = useCallback9(
4126
4345
  (placement) => isCacheEntryValid(preloadedCampaignsRef.current.get(placement)),
4127
4346
  []
4128
4347
  );
4129
- const consumePreloadedCampaign = useCallback8(
4348
+ const consumePreloadedCampaign = useCallback9(
4130
4349
  (placement) => consumeCacheEntry(preloadedCampaignsRef.current, placement),
4131
4350
  []
4132
4351
  );
4133
- const preloadCampaign = useCallback8(
4352
+ const preloadCampaign = useCallback9(
4134
4353
  async (placement, context) => {
4135
4354
  try {
4136
4355
  const campaign = await PaywalloClient.getCampaign(placement, context);
@@ -4196,11 +4415,11 @@ function useCampaignPreload() {
4196
4415
  }
4197
4416
 
4198
4417
  // src/hooks/useProductLoader.ts
4199
- import { useCallback as useCallback9, useState as useState9 } from "react";
4418
+ import { useCallback as useCallback10, useState as useState10 } from "react";
4200
4419
  function useProductLoader() {
4201
- const [products, setProducts] = useState9(/* @__PURE__ */ new Map());
4202
- const [isLoadingProducts, setIsLoadingProducts] = useState9(false);
4203
- 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) => {
4204
4423
  setIsLoadingProducts(true);
4205
4424
  try {
4206
4425
  const iapService = getIAPService();
@@ -4218,10 +4437,10 @@ function useProductLoader() {
4218
4437
  }
4219
4438
 
4220
4439
  // src/hooks/usePurchaseOrchestrator.ts
4221
- import { useCallback as useCallback10 } from "react";
4440
+ import { useCallback as useCallback11 } from "react";
4222
4441
  function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef) {
4223
4442
  const { trackDismissed, trackProductSelected, trackPurchased } = usePaywallTracking();
4224
- const handlePreloadedPurchase = useCallback10(
4443
+ const handlePreloadedPurchase = useCallback11(
4225
4444
  async (productId) => {
4226
4445
  try {
4227
4446
  if (preloadedWebView) {
@@ -4256,7 +4475,7 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4256
4475
  },
4257
4476
  [preloadedWebView, resolvePreloadedPaywall, clearPreloadedWebView, setShowingPreloadedWebView, presentedAtRef, trackDismissed, trackProductSelected, trackPurchased]
4258
4477
  );
4259
- const handlePreloadedRestore = useCallback10(async () => {
4478
+ const handlePreloadedRestore = useCallback11(async () => {
4260
4479
  try {
4261
4480
  const transactions = await getIAPService().restore();
4262
4481
  if (transactions.length === 0) return;
@@ -4278,16 +4497,58 @@ function usePurchaseOrchestrator(preloadedWebView, resolvePreloadedPaywall, clea
4278
4497
  }
4279
4498
 
4280
4499
  // src/hooks/useSubscriptionSync.ts
4281
- 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";
4282
4501
  var CACHE_TTL_MS2 = 5 * 60 * 1e3;
4283
- 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
+ }
4284
4544
  function useSubscriptionSync() {
4285
- const [subscription, setSubscription] = useState10(null);
4545
+ const [subscription, setSubscription] = useState11(null);
4286
4546
  const cacheRef = useRef7(null);
4287
- const invalidateCache = useCallback11(() => {
4547
+ const invalidateCache = useCallback12(() => {
4288
4548
  cacheRef.current = null;
4549
+ void subscriptionCache.invalidate();
4289
4550
  }, []);
4290
- const fetchAndCache = useCallback11(async () => {
4551
+ const fetchAndCache = useCallback12(async () => {
4291
4552
  const apiClient = PaywalloClient.getApiClient();
4292
4553
  const distinctId = PaywalloClient.getDistinctId();
4293
4554
  if (!apiClient || !distinctId) return null;
@@ -4298,18 +4559,37 @@ function useSubscriptionSync() {
4298
4559
  } : null;
4299
4560
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4300
4561
  setSubscription(sub);
4562
+ void subscriptionCache.set(toDomainResponse(status));
4301
4563
  return sub;
4302
4564
  }, []);
4303
- const hasActiveSubscription = useCallback11(async () => {
4565
+ const hasActiveSubscription = useCallback12(async () => {
4304
4566
  const apiClient = PaywalloClient.getApiClient();
4305
4567
  const distinctId = PaywalloClient.getDistinctId();
4306
4568
  if (!apiClient || !distinctId) return false;
4307
4569
  const cached = cacheRef.current;
4308
4570
  if (cached && cached.expiresAt > Date.now()) {
4309
- if (!cached.data) return false;
4310
- if (!ACTIVE_STATUSES.includes(cached.data.status)) return false;
4311
- if (cached.data.expiresAt && cached.data.expiresAt < /* @__PURE__ */ new Date()) return false;
4312
- 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 {
4313
4593
  }
4314
4594
  try {
4315
4595
  const status = await apiClient.getSubscriptionStatus(distinctId);
@@ -4319,12 +4599,13 @@ function useSubscriptionSync() {
4319
4599
  } : null;
4320
4600
  cacheRef.current = { data: sub, expiresAt: Date.now() + CACHE_TTL_MS2 };
4321
4601
  setSubscription(sub);
4602
+ void subscriptionCache.set(toDomainResponse(status));
4322
4603
  return status.hasActiveSubscription;
4323
4604
  } catch {
4324
- return false;
4605
+ return await resolveOfflineFromCache();
4325
4606
  }
4326
- }, []);
4327
- const getSubscription = useCallback11(async () => {
4607
+ }, [fetchAndCache]);
4608
+ const getSubscription = useCallback12(async () => {
4328
4609
  const apiClient = PaywalloClient.getApiClient();
4329
4610
  const distinctId = PaywalloClient.getDistinctId();
4330
4611
  if (!apiClient || !distinctId) return null;
@@ -4333,7 +4614,22 @@ function useSubscriptionSync() {
4333
4614
  try {
4334
4615
  return await fetchAndCache();
4335
4616
  } catch {
4336
- 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
+ }
4337
4633
  }
4338
4634
  }, [fetchAndCache]);
4339
4635
  return { subscription, hasActiveSubscription, getSubscription, invalidateCache };
@@ -4342,14 +4638,14 @@ function useSubscriptionSync() {
4342
4638
  // src/PaywalloProvider.tsx
4343
4639
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
4344
4640
  function PaywalloProvider({ children, config }) {
4345
- const [isInitialized, setIsInitialized] = useState11(false);
4346
- const [initError, setInitError] = useState11(null);
4347
- const [distinctId, setDistinctId] = useState11(null);
4348
- 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);
4349
4645
  const { products, isLoadingProducts, refreshProducts } = useProductLoader();
4350
4646
  const { subscription, hasActiveSubscription, getSubscription, invalidateCache } = useSubscriptionSync();
4351
4647
  const { preloadedWebView, setPreloadedWebView, preloadCampaign, consumePreloadedCampaign, isPreloadValid } = useCampaignPreload();
4352
- const clearPreloadedWebView = useCallback12(() => setPreloadedWebView(null), [setPreloadedWebView]);
4648
+ const clearPreloadedWebView = useCallback13(() => setPreloadedWebView(null), [setPreloadedWebView]);
4353
4649
  const { showingPreloadedWebView, presentPreloadedWebView, resolvePreloadedPaywall, handlePreloadedWebViewReady, handlePreloadedClose, setShowingPreloadedWebView, presentedAtRef } = usePaywallPresenter(preloadedWebView, clearPreloadedWebView, setPreloadedWebView);
4354
4650
  const { handlePreloadedPurchase: rawPreloadedPurchase, handlePreloadedRestore } = usePurchaseOrchestrator(
4355
4651
  preloadedWebView,
@@ -4358,8 +4654,8 @@ function PaywalloProvider({ children, config }) {
4358
4654
  setShowingPreloadedWebView,
4359
4655
  presentedAtRef
4360
4656
  );
4361
- const [isPreloadPurchasing, setIsPreloadPurchasing] = useState11(false);
4362
- const handlePreloadedPurchase = useCallback12(async (productId) => {
4657
+ const [isPreloadPurchasing, setIsPreloadPurchasing] = useState12(false);
4658
+ const handlePreloadedPurchase = useCallback13(async (productId) => {
4363
4659
  setIsPreloadPurchasing(true);
4364
4660
  try {
4365
4661
  await rawPreloadedPurchase(productId);
@@ -4376,21 +4672,21 @@ function PaywalloProvider({ children, config }) {
4376
4672
  setInitError(err instanceof Error ? err : new ClientError(CLIENT_ERROR_CODES.NOT_INITIALIZED, String(err)));
4377
4673
  });
4378
4674
  }, [config]);
4379
- const getPaywallConfig = useCallback12(async (p) => {
4675
+ const getPaywallConfig = useCallback13(async (p) => {
4380
4676
  const api = PaywalloClient.getApiClient();
4381
4677
  return api ? api.getPaywall(p) : null;
4382
4678
  }, []);
4383
- const presentPaywall = useCallback12((placement) => {
4679
+ const presentPaywall = useCallback13((placement) => {
4384
4680
  return new Promise((resolve) => setActivePaywall({ placement, resolver: resolve }));
4385
4681
  }, []);
4386
- const trackCampaignImpression = useCallback12((placement, variantKey, campaignId) => {
4682
+ const trackCampaignImpression = useCallback13((placement, variantKey, campaignId) => {
4387
4683
  const api = PaywalloClient.getApiClient();
4388
4684
  if (!api) return;
4389
4685
  const sessionId = PaywalloClient.getSessionId();
4390
4686
  void api.trackEvent("$campaign_impression", PaywalloClient.getDistinctId(), { placement, variantKey, campaignId, ...sessionId && { sessionId } }).catch(() => {
4391
4687
  });
4392
4688
  }, []);
4393
- const presentCampaign = useCallback12(
4689
+ const presentCampaign = useCallback13(
4394
4690
  async (placement, context) => {
4395
4691
  try {
4396
4692
  if (preloadedWebView?.placement === placement && preloadedWebView.loaded && isPreloadValid(placement)) {
@@ -4459,7 +4755,7 @@ function PaywalloProvider({ children, config }) {
4459
4755
  },
4460
4756
  [preloadedWebView, isPreloadValid, consumePreloadedCampaign, presentPreloadedWebView, trackCampaignImpression]
4461
4757
  );
4462
- const restorePurchases = useCallback12(async () => {
4758
+ const restorePurchases = useCallback13(async () => {
4463
4759
  invalidateCache();
4464
4760
  try {
4465
4761
  const txs = await getIAPService().restore();
@@ -4468,7 +4764,7 @@ function PaywalloProvider({ children, config }) {
4468
4764
  return { success: false, restoredProducts: [], error: error instanceof Error ? error : new PurchaseError(PURCHASE_ERROR_CODES.RESTORE_FAILED, String(error)) };
4469
4765
  }
4470
4766
  }, [invalidateCache]);
4471
- const handlePaywallResult = useCallback12((result) => {
4767
+ const handlePaywallResult = useCallback13((result) => {
4472
4768
  if (activePaywall) {
4473
4769
  activePaywall.resolver(result);
4474
4770
  setActivePaywall(null);
@@ -4491,7 +4787,7 @@ function PaywalloProvider({ children, config }) {
4491
4787
  useEffect7(() => {
4492
4788
  if (activePaywall) setPreloadedWebView(null);
4493
4789
  }, [activePaywall, setPreloadedWebView]);
4494
- const noOp = useCallback12(() => {
4790
+ const noOp = useCallback13(() => {
4495
4791
  }, []);
4496
4792
  const contextValue = useMemo2(() => ({
4497
4793
  config: PaywalloClient.getConfig(),