sitepong 0.1.8 → 0.1.10

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
@@ -596,6 +596,144 @@ Content inside `<SensitiveView>` is replaced with a black rectangle in recorded
596
596
  | `bufferDuration` | `10000` (10s) | `60000` (60s) | Rolling buffer size in ms |
597
597
  | `maxDuration` | `3600000` | `3600000` | Max recording duration in ms |
598
598
 
599
+ ### Push Notifications & Live Activities
600
+
601
+ Native APNs (iOS) and FCM (Android) push notifications, plus iOS Live Activity updates. SitePong sends directly to Apple/Google — no Expo Push service in the path.
602
+
603
+ #### Token lifecycle
604
+
605
+ There are two halves to push: the **client SDK** registers device tokens with the SitePong ingest server, and the **server-side Push API** sends notifications to those tokens.
606
+
607
+ ```
608
+ [App launches]
609
+
610
+ [Notifications.getDevicePushTokenAsync()] ← OS gives you a native token
611
+
612
+ [registerDeviceToken(token, opts)] ← SDK POSTs to /api/push/tokens
613
+ ↓ with X-API-Key: sp_live_xxx
614
+ [Token stored in SitePong]
615
+
616
+ [identify('user-123')] ← SDK auto re-registers token
617
+ ↓ with user_id (via lazy hook)
618
+ [Your backend → POST /api/push/send] ← server-only sp_push_xxx key
619
+
620
+ [SitePong → APNs / FCM → Device]
621
+ ```
622
+
623
+ #### 1. Install permission and get the native token
624
+
625
+ ```bash
626
+ npx expo install expo-notifications
627
+ ```
628
+
629
+ ```typescript
630
+ import * as Notifications from 'expo-notifications';
631
+ import * as Device from 'expo-device';
632
+ import { Platform } from 'react-native';
633
+
634
+ if (!Device.isDevice) return; // simulator can't receive push
635
+
636
+ const { status } = await Notifications.requestPermissionsAsync();
637
+ if (status !== 'granted') return;
638
+
639
+ // Returns the raw native token (APNs hex on iOS, FCM token on Android)
640
+ const { data: token } = await Notifications.getDevicePushTokenAsync();
641
+ ```
642
+
643
+ #### 2. Register the token with SitePong
644
+
645
+ ```typescript
646
+ import { registerDeviceToken } from '@sitepong/sdk/react-native';
647
+ import { Platform } from 'react-native';
648
+
649
+ registerDeviceToken(token, {
650
+ platform: Platform.OS as 'ios' | 'android',
651
+ environment: __DEV__ ? 'sandbox' : 'production',
652
+ userId: 'user-123', // optional — can be set later via identify()
653
+ });
654
+ ```
655
+
656
+ This makes a `POST /api/push/tokens` request to the ingest server with `X-API-Key: <your SDK key>`. The body includes the token, `token_type` (`apns`/`fcm`), `device_id`, `user_id`, `platform`, app version, device model, and OS version. The server upserts by `(token, environment)` so re-registration is idempotent.
657
+
658
+ #### 3. User identification (automatic)
659
+
660
+ When you call `identify(userId)`, the SDK automatically re-registers all cached push, Live Activity, and push-to-start tokens with the new `user_id`. **You do not need to call `registerDeviceToken()` again after login.**
661
+
662
+ ```typescript
663
+ import { identify } from '@sitepong/sdk/react-native';
664
+
665
+ // Token already registered anonymously at app launch
666
+ // Now user logs in:
667
+ identify('user-123');
668
+ // → SDK re-POSTs /api/push/tokens with user_id: "user-123"
669
+ ```
670
+
671
+ The hook is installed lazily the first time any `register*` function is called, so it works whether you use `SitePongRNProvider` or call `initRN()` directly.
672
+
673
+ #### 4. Send notifications from your backend
674
+
675
+ Generate a separate **Push API key** (prefix `sp_push_`) from the Notifications tab in the SitePong dashboard. This is **not** the SDK key — it's server-only and has permission to send pushes.
676
+
677
+ ```bash
678
+ curl -X POST https://api.sitepong.com/api/push/send \
679
+ -H "Authorization: Bearer sp_push_xxxxxxxxxxxxxxxx" \
680
+ -H "Content-Type: application/json" \
681
+ -d '{
682
+ "title": "Order Shipped",
683
+ "body": "Your order #1234 is on its way",
684
+ "data": { "orderId": "1234" },
685
+ "target": { "user_ids": ["user-123"] }
686
+ }'
687
+ ```
688
+
689
+ Targeting options (provide at least one):
690
+
691
+ - `user_ids: string[]` — sends to all active devices for these users
692
+ - `device_ids: string[]` — sends to specific SitePong device IDs
693
+ - `tokens: string[]` — sends directly to raw APNs/FCM tokens
694
+
695
+ #### Live Activities (iOS)
696
+
697
+ Register the per-instance push token from your native bridge:
698
+
699
+ ```typescript
700
+ import {
701
+ registerLiveActivityToken,
702
+ registerPushToStartToken,
703
+ endLiveActivity,
704
+ } from '@sitepong/sdk/react-native';
705
+
706
+ // When ActivityKit gives you a push token for a running activity
707
+ registerLiveActivityToken('DeliveryActivityAttributes', activity.id, pushToken, {
708
+ environment: 'production',
709
+ });
710
+
711
+ // Or register a push-to-start token (iOS 17.2+) so the activity can be launched remotely
712
+ registerPushToStartToken('DeliveryActivityAttributes', pushToStartToken, {
713
+ environment: 'production',
714
+ });
715
+
716
+ // When the activity ends, deactivate its token
717
+ endLiveActivity('DeliveryActivityAttributes', activity.id);
718
+ ```
719
+
720
+ #### Token invalidation
721
+
722
+ When APNs or FCM reports a token as invalid (app uninstalled, notifications disabled, token rotated), SitePong marks it inactive on the server side and stops sending. No client action needed. To force a refresh after an OS-level token change, call `registerDeviceToken()` again with the new token.
723
+
724
+ #### Endpoint reference
725
+
726
+ | Method | Path | Auth | SDK function |
727
+ |---|---|---|---|
728
+ | POST | `/api/push/tokens` | `X-API-Key: sp_live_*` | `registerDeviceToken()` |
729
+ | POST | `/api/push/live-activity-tokens` | `X-API-Key: sp_live_*` | `registerLiveActivityToken()` |
730
+ | POST | `/api/push/push-to-start-tokens` | `X-API-Key: sp_live_*` | `registerPushToStartToken()` |
731
+ | DELETE | `/api/push/live-activity-tokens` | `X-API-Key: sp_live_*` | `endLiveActivity()` |
732
+ | POST | `/api/push/send` | `Authorization: Bearer sp_push_*` | (server-side only) |
733
+ | POST | `/api/push/live-activity` | `Authorization: Bearer sp_push_*` | (server-side only) |
734
+
735
+ Default ingest endpoint: `https://ingest.sitepong.com`. Override via the `endpoint` option in `initRN()` or `SitePongRNProvider`.
736
+
599
737
  ### Device Intelligence (React Native)
600
738
 
601
739
  Persistent device identification that **survives app reinstalls**. Requires the `@sitepong/device-id` native module.
@@ -2811,83 +2811,21 @@ function createAsyncStorageAdapter(asyncStorage) {
2811
2811
  removeItem: (key) => asyncStorage.removeItem(key)
2812
2812
  };
2813
2813
  }
2814
- var deviceIdModule = null;
2815
- function getDeviceIdModule() {
2816
- if (!deviceIdModule) {
2817
- try {
2818
- const n = ["@sitepong", "device-id"].join("/");
2819
- deviceIdModule = __require(n);
2820
- } catch {
2821
- deviceIdModule = null;
2822
- }
2823
- }
2824
- return deviceIdModule;
2825
- }
2826
2814
  function collectDeviceInfo() {
2827
- const info = {
2815
+ const screen = reactNative.Dimensions.get("screen");
2816
+ return {
2828
2817
  platform: reactNative.Platform.OS,
2829
- osVersion: String(reactNative.Platform.Version)
2818
+ osVersion: String(reactNative.Platform.Version),
2819
+ screenWidth: screen.width,
2820
+ screenHeight: screen.height,
2821
+ screenScale: screen.scale
2830
2822
  };
2831
- const screen = reactNative.Dimensions.get("screen");
2832
- info.screenWidth = screen.width;
2833
- info.screenHeight = screen.height;
2834
- info.screenScale = screen.scale;
2835
- let ExpoDevice = null;
2836
- try {
2837
- const n = ["expo", "device"].join("-");
2838
- ExpoDevice = __require(n);
2839
- } catch {
2840
- }
2841
- if (ExpoDevice) {
2842
- if (ExpoDevice.brand) info.deviceBrand = ExpoDevice.brand;
2843
- if (ExpoDevice.modelName) info.deviceModel = ExpoDevice.modelName;
2844
- if (ExpoDevice.deviceName) info.deviceName = ExpoDevice.deviceName;
2845
- info.isEmulator = !ExpoDevice.isDevice;
2846
- }
2847
- let ExpoApplication = null;
2848
- try {
2849
- const n = ["expo", "application"].join("-");
2850
- ExpoApplication = __require(n);
2851
- } catch {
2852
- }
2853
- if (ExpoApplication) {
2854
- if (ExpoApplication.nativeApplicationVersion) {
2855
- info.appVersion = ExpoApplication.nativeApplicationVersion;
2856
- }
2857
- if (ExpoApplication.nativeBuildVersion) {
2858
- info.appBuildNumber = ExpoApplication.nativeBuildVersion;
2859
- }
2860
- }
2861
- return info;
2862
2823
  }
2863
2824
  async function fetchPersistentDeviceId() {
2864
- const mod = getDeviceIdModule();
2865
- if (!mod) return null;
2866
- try {
2867
- return await mod.getDeviceId();
2868
- } catch {
2869
- return null;
2870
- }
2825
+ return null;
2871
2826
  }
2872
2827
  async function fetchNativeDeviceSignals() {
2873
- const mod = getDeviceIdModule();
2874
- if (!mod) return null;
2875
- try {
2876
- const signals = await mod.getDeviceSignals();
2877
- return {
2878
- platform: signals.platform,
2879
- osVersion: signals.osVersion,
2880
- deviceModel: signals.model,
2881
- deviceBrand: signals.manufacturer,
2882
- isEmulator: signals.isEmulator,
2883
- screenScale: signals.screenScale,
2884
- deviceId: signals.deviceId,
2885
- identifierForVendor: signals.identifierForVendor,
2886
- androidId: signals.androidId
2887
- };
2888
- } catch {
2889
- return null;
2890
- }
2828
+ return null;
2891
2829
  }
2892
2830
 
2893
2831
  // src/react-native/screen-recorder.ts
@@ -3222,7 +3160,20 @@ var RNPerformanceManager = class {
3222
3160
  };
3223
3161
 
3224
3162
  // src/react-native/push.ts
3163
+ var identifyHookInstalled = false;
3164
+ function ensureIdentifyHookInstalled() {
3165
+ if (identifyHookInstalled) return;
3166
+ identifyHookInstalled = true;
3167
+ try {
3168
+ const c = sitepong;
3169
+ if (typeof c.registerIdentifyHook === "function") {
3170
+ c.registerIdentifyHook(reRegisterTokensWithUserId);
3171
+ }
3172
+ } catch {
3173
+ }
3174
+ }
3225
3175
  var cachedDeviceContext = null;
3176
+ var registeredPushToken = null;
3226
3177
  var registeredLiveActivityTokens = /* @__PURE__ */ new Map();
3227
3178
  var registeredPushToStartTokens = /* @__PURE__ */ new Map();
3228
3179
  function getEndpoint() {
@@ -3303,9 +3254,10 @@ async function deleteFromIngest(path, body) {
3303
3254
  }
3304
3255
  }
3305
3256
  function registerDeviceToken(token, options) {
3257
+ ensureIdentifyHookInstalled();
3306
3258
  const device = getDeviceContext();
3307
3259
  const tokenType = options.platform === "ios" ? "apns" : "fcm";
3308
- ({ token, environment: options.environment });
3260
+ registeredPushToken = { token, environment: options.environment };
3309
3261
  postToIngest("/api/push/tokens", {
3310
3262
  native_device_token: token,
3311
3263
  token_type: tokenType,
@@ -3319,6 +3271,7 @@ function registerDeviceToken(token, options) {
3319
3271
  });
3320
3272
  }
3321
3273
  function registerLiveActivityToken(activityType, activityId, pushToken, options) {
3274
+ ensureIdentifyHookInstalled();
3322
3275
  const device = getDeviceContext();
3323
3276
  const userId = getUserId();
3324
3277
  const key = `${activityType}:${activityId}`;
@@ -3338,6 +3291,7 @@ function registerLiveActivityToken(activityType, activityId, pushToken, options)
3338
3291
  });
3339
3292
  }
3340
3293
  function registerPushToStartToken(activityType, pushToStartToken, options) {
3294
+ ensureIdentifyHookInstalled();
3341
3295
  const device = getDeviceContext();
3342
3296
  const userId = getUserId();
3343
3297
  registeredPushToStartTokens.set(activityType, {
@@ -3361,6 +3315,42 @@ function endLiveActivity(activityType, activityId) {
3361
3315
  activity_id: activityId
3362
3316
  });
3363
3317
  }
3318
+ function reRegisterTokensWithUserId(userId) {
3319
+ if (registeredPushToken) {
3320
+ const device = getDeviceContext();
3321
+ postToIngest("/api/push/tokens", {
3322
+ expo_push_token: registeredPushToken.token,
3323
+ environment: registeredPushToken.environment,
3324
+ device_id: device.deviceId,
3325
+ user_id: userId,
3326
+ platform: device.platform,
3327
+ app_version: device.appVersion,
3328
+ device_model: device.deviceModel,
3329
+ os_version: device.osVersion
3330
+ });
3331
+ }
3332
+ for (const entry of registeredLiveActivityTokens.values()) {
3333
+ const device = getDeviceContext();
3334
+ postToIngest("/api/push/live-activity-tokens", {
3335
+ activity_type: entry.activityType,
3336
+ activity_id: entry.activityId,
3337
+ push_token: entry.token,
3338
+ environment: entry.environment,
3339
+ device_id: device.deviceId,
3340
+ user_id: userId
3341
+ });
3342
+ }
3343
+ for (const entry of registeredPushToStartTokens.values()) {
3344
+ const device = getDeviceContext();
3345
+ postToIngest("/api/push/push-to-start-tokens", {
3346
+ activity_type: entry.activityType,
3347
+ push_to_start_token: entry.token,
3348
+ environment: entry.environment,
3349
+ device_id: device.deviceId,
3350
+ user_id: userId
3351
+ });
3352
+ }
3353
+ }
3364
3354
  var SitePongRNContext = react.createContext({
3365
3355
  isInitialized: false,
3366
3356
  performanceManager: null