@virex-tech/paywallo-sdk 2.5.1 → 2.5.2

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/README.md CHANGED
@@ -233,6 +233,27 @@ const onStateChange = usePaywalloScreenTracking();
233
233
 
234
234
  Works with React Navigation v6/v7 (no extra peer dependency).
235
235
 
236
+ ## Identify users
237
+
238
+ Call `identify` after login (and on every logged-in app launch) to link the device to your user:
239
+
240
+ ```tsx
241
+ await PaywalloClient.identify({
242
+ email: user.email, // optional — improves ad-platform matching (CAPI)
243
+ firstName: user.firstName, // optional
244
+ properties: {
245
+ userId: user.id, // REQUIRED for per-user features — your backend's user id
246
+ },
247
+ });
248
+ ```
249
+
250
+ Key rules:
251
+
252
+ - **`properties.userId` is what links the user.** It becomes `external_user_id` server-side and is the value you pass as `recipients` in `POST /v1/notifications/trigger`. Calling `identify({ email })` alone does **not** link the user — push tokens stay anonymous and per-user sends silently target zero devices (`skippedReason: "no_active_tokens"`).
253
+ - **Order doesn't matter.** The link is retroactive: tokens registered before `identify` (e.g. during onboarding, pre-login) are linked when `identify` runs.
254
+ - **Call it on every logged-in launch, not just once.** `identify` is idempotent and queued durably (offline-safe), so repeating it is cheap — and it self-heals devices that missed a previous attempt.
255
+ - **`reset()` unlinks the device.** It regenerates the anonymous id (use it on logout/account switch). After the next login, call `identify` again or new tokens stay anonymous.
256
+
236
257
  ## Imperative API
237
258
 
238
259
  For use outside React components:
@@ -240,9 +261,9 @@ For use outside React components:
240
261
  ```tsx
241
262
  import { PaywalloClient } from '@virex-tech/paywallo-sdk';
242
263
 
243
- // Identify the user (optionalattribution works without it).
264
+ // Identify user (see "Identify users" above properties.userId is what links the user).
244
265
  // Model fields: email, firstName, lastName, dateOfBirth, gender, phone, properties.
245
- await PaywalloClient.identify({ email: 'user@example.com', firstName: 'Ana' });
266
+ await PaywalloClient.identify({ email: 'user@example.com', properties: { userId: 'user-123' } });
246
267
 
247
268
  // Custom events
248
269
  await PaywalloClient.track('my_event', { properties: { plan: 'premium' } });
@@ -8,6 +8,14 @@
8
8
  -->
9
9
  <uses-permission android:name="com.android.vending.BILLING" />
10
10
 
11
+ <!--
12
+ POST_NOTIFICATIONS (Android 13+) — required to display push notifications.
13
+ PaywalloNotificationsModule requests it at runtime, but the request only
14
+ surfaces a system prompt if the permission is declared in the merged
15
+ manifest. Declaring it here means the host app gets it for free.
16
+ -->
17
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
18
+
11
19
  <application>
12
20
  <!--
13
21
  PaywalloFirebaseMessagingService handles FCM token refresh and message delivery.
@@ -233,7 +233,11 @@ class PaywalloStoreKitModule(private val reactContext: ReactApplicationContext)
233
233
  listOf(productDetailsParams.build())
234
234
  )
235
235
 
236
- if (!appAccountToken.isNullOrBlank()) {
236
+ // Google Play rejects obfuscated account IDs longer than 64 chars
237
+ // with an IllegalArgumentException that aborts launchBillingFlow.
238
+ // A UUID is well under the limit; guard so an oversized id degrades
239
+ // to "no account id" instead of failing the whole purchase.
240
+ if (!appAccountToken.isNullOrBlank() && appAccountToken.length <= 64) {
237
241
  flowParamsBuilder.setObfuscatedAccountId(appAccountToken)
238
242
  }
239
243
 
@@ -391,9 +395,17 @@ class PaywalloStoreKitModule(private val reactContext: ReactApplicationContext)
391
395
  .setProductType(BillingClient.ProductType.SUBS)
392
396
  .build()
393
397
  )
398
+ // INAPP covers non-consumables (lifetime unlocks). iOS restores
399
+ // these via Transaction.currentEntitlements, so querying only SUBS
400
+ // here would silently drop non-consumables from restore on Android.
401
+ val inappResult = client.queryPurchasesAsync(
402
+ QueryPurchasesParams.newBuilder()
403
+ .setProductType(BillingClient.ProductType.INAPP)
404
+ .build()
405
+ )
394
406
 
395
407
  val seenTokens = mutableSetOf<String>()
396
- for (purchase in subsResult.purchasesList) {
408
+ for (purchase in subsResult.purchasesList + inappResult.purchasesList) {
397
409
  if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
398
410
  seenTokens.add(purchase.purchaseToken)
399
411
  val map = WritableNativeMap()
package/dist/index.d.mts CHANGED
@@ -922,6 +922,8 @@ interface AttributionCapture {
922
922
  readonly capturedAt: string;
923
923
  }
924
924
  type AttributionInput = Omit<AttributionCapture, "capturedAt">;
925
+ /** Listener notified when `capture()` performs its first real write. */
926
+ type AttributionCaptureListener = (capture: AttributionCapture) => void;
925
927
  /**
926
928
  * Attribution module — first-write-wins persistence of UTM / click-ID data.
927
929
  *
@@ -935,6 +937,7 @@ type AttributionInput = Omit<AttributionCapture, "capturedAt">;
935
937
  declare class AttributionTracker {
936
938
  private cache;
937
939
  private captureInProgress;
940
+ private readonly listeners;
938
941
  private readonly storage;
939
942
  constructor(debug?: boolean);
940
943
  /**
@@ -949,6 +952,14 @@ declare class AttributionTracker {
949
952
  * @param data - Attribution fields without `capturedAt` (generated internally).
950
953
  */
951
954
  capture(data: AttributionInput): Promise<void>;
955
+ /**
956
+ * Subscribes to the first real attribution write (first-write-wins — no-op
957
+ * captures never fire). Useful for late enrichment consumers (e.g. re-push
958
+ * of Superwall attributes when a warm deep link / deferred match lands after
959
+ * init). Returns an unsubscribe function.
960
+ */
961
+ onCapture(listener: AttributionCaptureListener): () => void;
962
+ private notifyListeners;
952
963
  /**
953
964
  * Returns the persisted attribution capture, or `null` if none exists.
954
965
  * Synchronous — reads from in-memory cache (populated via `loadFromStorage()`).
@@ -1877,6 +1888,35 @@ interface UsePlanPurchaseResult {
1877
1888
  }
1878
1889
  declare function usePlanPurchase(): UsePlanPurchaseResult;
1879
1890
 
1891
+ /**
1892
+ * Extracts the name of the currently active leaf route from a react-navigation
1893
+ * NavigationState. Descends into nested navigators recursively.
1894
+ *
1895
+ * Does NOT import react-navigation — accepts `state: unknown` and uses
1896
+ * structural duck-typing, so it works with v6, v7, and future versions alike.
1897
+ *
1898
+ * @returns the active route name string, or null if the shape is unexpected.
1899
+ */
1900
+ declare function getActiveRouteName(state: unknown): string | null;
1901
+ /**
1902
+ * Returns a stable `onStateChange` callback ready to be passed directly to
1903
+ * `<NavigationContainer onStateChange={onStateChange}>`.
1904
+ *
1905
+ * Emits a `screen_view` event via the PaywalloClient pipeline each time the
1906
+ * active route changes. Consecutive calls with the same screen are no-ops
1907
+ * (deduplication by name). If the SDK is not yet initialized, calls are
1908
+ * silently ignored.
1909
+ *
1910
+ * @example
1911
+ * ```tsx
1912
+ * const onStateChange = usePaywalloScreenTracking();
1913
+ * <NavigationContainer onStateChange={onStateChange}>
1914
+ * {children}
1915
+ * </NavigationContainer>
1916
+ * ```
1917
+ */
1918
+ declare function usePaywalloScreenTracking(): (state: unknown) => void;
1919
+
1880
1920
  interface UsePlansResult {
1881
1921
  currentPlan: Plan | null;
1882
1922
  allPlans: Plan[];
@@ -1934,35 +1974,6 @@ interface UseSubscriptionResult {
1934
1974
  }
1935
1975
  declare function useSubscription(): UseSubscriptionResult;
1936
1976
 
1937
- /**
1938
- * Extracts the name of the currently active leaf route from a react-navigation
1939
- * NavigationState. Descends into nested navigators recursively.
1940
- *
1941
- * Does NOT import react-navigation — accepts `state: unknown` and uses
1942
- * structural duck-typing, so it works with v6, v7, and future versions alike.
1943
- *
1944
- * @returns the active route name string, or null if the shape is unexpected.
1945
- */
1946
- declare function getActiveRouteName(state: unknown): string | null;
1947
- /**
1948
- * Returns a stable `onStateChange` callback ready to be passed directly to
1949
- * `<NavigationContainer onStateChange={onStateChange}>`.
1950
- *
1951
- * Emits a `screen_view` event via the PaywalloClient pipeline each time the
1952
- * active route changes. Consecutive calls with the same screen are no-ops
1953
- * (deduplication by name). If the SDK is not yet initialized, calls are
1954
- * silently ignored.
1955
- *
1956
- * @example
1957
- * ```tsx
1958
- * const onStateChange = usePaywalloScreenTracking();
1959
- * <NavigationContainer onStateChange={onStateChange}>
1960
- * {children}
1961
- * </NavigationContainer>
1962
- * ```
1963
- */
1964
- declare function usePaywalloScreenTracking(): (state: unknown) => void;
1965
-
1966
1977
  interface PaywalloProviderProps {
1967
1978
  children: React__default.ReactNode;
1968
1979
  config: PaywalloConfig;
package/dist/index.d.ts CHANGED
@@ -922,6 +922,8 @@ interface AttributionCapture {
922
922
  readonly capturedAt: string;
923
923
  }
924
924
  type AttributionInput = Omit<AttributionCapture, "capturedAt">;
925
+ /** Listener notified when `capture()` performs its first real write. */
926
+ type AttributionCaptureListener = (capture: AttributionCapture) => void;
925
927
  /**
926
928
  * Attribution module — first-write-wins persistence of UTM / click-ID data.
927
929
  *
@@ -935,6 +937,7 @@ type AttributionInput = Omit<AttributionCapture, "capturedAt">;
935
937
  declare class AttributionTracker {
936
938
  private cache;
937
939
  private captureInProgress;
940
+ private readonly listeners;
938
941
  private readonly storage;
939
942
  constructor(debug?: boolean);
940
943
  /**
@@ -949,6 +952,14 @@ declare class AttributionTracker {
949
952
  * @param data - Attribution fields without `capturedAt` (generated internally).
950
953
  */
951
954
  capture(data: AttributionInput): Promise<void>;
955
+ /**
956
+ * Subscribes to the first real attribution write (first-write-wins — no-op
957
+ * captures never fire). Useful for late enrichment consumers (e.g. re-push
958
+ * of Superwall attributes when a warm deep link / deferred match lands after
959
+ * init). Returns an unsubscribe function.
960
+ */
961
+ onCapture(listener: AttributionCaptureListener): () => void;
962
+ private notifyListeners;
952
963
  /**
953
964
  * Returns the persisted attribution capture, or `null` if none exists.
954
965
  * Synchronous — reads from in-memory cache (populated via `loadFromStorage()`).
@@ -1877,6 +1888,35 @@ interface UsePlanPurchaseResult {
1877
1888
  }
1878
1889
  declare function usePlanPurchase(): UsePlanPurchaseResult;
1879
1890
 
1891
+ /**
1892
+ * Extracts the name of the currently active leaf route from a react-navigation
1893
+ * NavigationState. Descends into nested navigators recursively.
1894
+ *
1895
+ * Does NOT import react-navigation — accepts `state: unknown` and uses
1896
+ * structural duck-typing, so it works with v6, v7, and future versions alike.
1897
+ *
1898
+ * @returns the active route name string, or null if the shape is unexpected.
1899
+ */
1900
+ declare function getActiveRouteName(state: unknown): string | null;
1901
+ /**
1902
+ * Returns a stable `onStateChange` callback ready to be passed directly to
1903
+ * `<NavigationContainer onStateChange={onStateChange}>`.
1904
+ *
1905
+ * Emits a `screen_view` event via the PaywalloClient pipeline each time the
1906
+ * active route changes. Consecutive calls with the same screen are no-ops
1907
+ * (deduplication by name). If the SDK is not yet initialized, calls are
1908
+ * silently ignored.
1909
+ *
1910
+ * @example
1911
+ * ```tsx
1912
+ * const onStateChange = usePaywalloScreenTracking();
1913
+ * <NavigationContainer onStateChange={onStateChange}>
1914
+ * {children}
1915
+ * </NavigationContainer>
1916
+ * ```
1917
+ */
1918
+ declare function usePaywalloScreenTracking(): (state: unknown) => void;
1919
+
1880
1920
  interface UsePlansResult {
1881
1921
  currentPlan: Plan | null;
1882
1922
  allPlans: Plan[];
@@ -1934,35 +1974,6 @@ interface UseSubscriptionResult {
1934
1974
  }
1935
1975
  declare function useSubscription(): UseSubscriptionResult;
1936
1976
 
1937
- /**
1938
- * Extracts the name of the currently active leaf route from a react-navigation
1939
- * NavigationState. Descends into nested navigators recursively.
1940
- *
1941
- * Does NOT import react-navigation — accepts `state: unknown` and uses
1942
- * structural duck-typing, so it works with v6, v7, and future versions alike.
1943
- *
1944
- * @returns the active route name string, or null if the shape is unexpected.
1945
- */
1946
- declare function getActiveRouteName(state: unknown): string | null;
1947
- /**
1948
- * Returns a stable `onStateChange` callback ready to be passed directly to
1949
- * `<NavigationContainer onStateChange={onStateChange}>`.
1950
- *
1951
- * Emits a `screen_view` event via the PaywalloClient pipeline each time the
1952
- * active route changes. Consecutive calls with the same screen are no-ops
1953
- * (deduplication by name). If the SDK is not yet initialized, calls are
1954
- * silently ignored.
1955
- *
1956
- * @example
1957
- * ```tsx
1958
- * const onStateChange = usePaywalloScreenTracking();
1959
- * <NavigationContainer onStateChange={onStateChange}>
1960
- * {children}
1961
- * </NavigationContainer>
1962
- * ```
1963
- */
1964
- declare function usePaywalloScreenTracking(): (state: unknown) => void;
1965
-
1966
1977
  interface PaywalloProviderProps {
1967
1978
  children: React__default.ReactNode;
1968
1979
  config: PaywalloConfig;