sitepong 0.1.9 → 0.1.11

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,209 @@ 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
+ SitePong ships a turnkey Live Activity package — a generic SwiftUI Widget Extension that the bundled config plugin generates at prebuild, plus a JavaScript API that drives it from data. **No Swift required.** Customers compose their Live Activity using SF Symbols, custom progress bars, and styled text fields entirely from JS.
698
+
699
+ Install the package:
700
+
701
+ ```bash
702
+ npm install @sitepong/expo-live-activity
703
+ ```
704
+
705
+ Add the config plugin to `app.json` / `app.config.js`:
706
+
707
+ ```json
708
+ {
709
+ "plugins": ["@sitepong/expo-live-activity"]
710
+ }
711
+ ```
712
+
713
+ Then prebuild and rebuild your app (`npx expo prebuild --clean`). The plugin creates a Widget Extension target named `SitePongLiveActivityWidget` containing the SitePong template, sets `NSSupportsLiveActivities` in the main app, and wires the extension into the Xcode project.
714
+
715
+ Start a Live Activity from JavaScript:
716
+
717
+ ```typescript
718
+ import { startLiveActivity, updateLiveActivity, endLiveActivity } from '@sitepong/expo-live-activity';
719
+
720
+ const { activityId } = await startLiveActivity({
721
+ activityType: 'order-tracking',
722
+ attributes: { id: '#1234' },
723
+ contentState: {
724
+ title: 'Order #1234',
725
+ subtitle: 'Driver is on the way',
726
+ titleStyle: { size: 16, weight: 'bold', color: '#111827' },
727
+ subtitleStyle: { size: 14, weight: 'regular', color: '#6b7280' },
728
+ leadingIcon: { symbol: 'shippingbox.fill', color: '#3b82f6' },
729
+ trailingIcon: { symbol: 'checkmark.seal.fill', color: '#10b981' }, // hidden until you set it
730
+ progress: {
731
+ style: 'bar-with-icon', // 'bar' | 'bar-with-icon' | 'circular' | 'timer'
732
+ value: 0.6,
733
+ icon: 'car.fill', // SF Symbol slides along the track
734
+ iconColor: '#3b82f6',
735
+ trackColor: '#e5e7eb',
736
+ fillColor: '#3b82f6',
737
+ label: '15 minutes away',
738
+ },
739
+ },
740
+ });
741
+ ```
742
+
743
+ Update it locally as state changes:
744
+
745
+ ```typescript
746
+ await updateLiveActivity(activityId, {
747
+ title: 'Order #1234',
748
+ subtitle: 'Driver is two blocks away',
749
+ progress: { style: 'bar-with-icon', value: 0.9, icon: 'car.fill', fillColor: '#3b82f6' },
750
+ });
751
+ ```
752
+
753
+ Or update it remotely from your backend by calling `POST /api/push/live-activity` (see endpoint table below). The package automatically forwards the per-activity push token to SitePong on `startLiveActivity`, so backend updates reach the right device with no extra wiring.
754
+
755
+ End the activity when the work completes:
756
+
757
+ ```typescript
758
+ await endLiveActivity(activityId, {
759
+ finalContentState: {
760
+ title: 'Delivered',
761
+ leadingIcon: { symbol: 'checkmark.seal.fill', color: '#10b981' },
762
+ },
763
+ dismissalDate: Date.now() + 60_000, // remove from lock screen after 1 minute
764
+ });
765
+ ```
766
+
767
+ **Visibility-driven slots.** Every visual element in `contentState` is optional. The SwiftUI template hides slots whose value is omitted, so you control layout density entirely from data. Set `trailingIcon` only on the final "delivered" update and it appears; omit it everywhere else and it stays hidden.
768
+
769
+ **Available styles.** Title and subtitle accept `size`, `weight` (`ultraLight`...`black`), `color` (hex), `italic`, `monospacedDigit`. Icons accept any SF Symbol name plus `color`, `weight`, `size`. Progress supports `bar`, `bar-with-icon` (the Uber-Eats-style sliding icon), `circular`, and `timer` (Apple's auto-updating countdown).
770
+
771
+ **Bringing your own widget.** If you need a visually distinctive Live Activity beyond what the template covers, you can skip `@sitepong/expo-live-activity` and write the Widget Extension yourself in Swift, then call the lower-level token registration directly:
772
+
773
+ ```typescript
774
+ import {
775
+ registerLiveActivityToken,
776
+ registerPushToStartToken,
777
+ endLiveActivity as endLiveActivityToken,
778
+ } from 'sitepong/react-native';
779
+
780
+ registerLiveActivityToken('MyActivityAttributes', activity.id, pushToken, { environment: 'production' });
781
+ registerPushToStartToken('MyActivityAttributes', pushToStartToken, { environment: 'production' });
782
+ endLiveActivityToken('MyActivityAttributes', activity.id);
783
+ ```
784
+
785
+ #### Token invalidation
786
+
787
+ 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.
788
+
789
+ #### Endpoint reference
790
+
791
+ | Method | Path | Auth | SDK function |
792
+ |---|---|---|---|
793
+ | POST | `/api/push/tokens` | `X-API-Key: sp_live_*` | `registerDeviceToken()` |
794
+ | POST | `/api/push/live-activity-tokens` | `X-API-Key: sp_live_*` | `registerLiveActivityToken()` |
795
+ | POST | `/api/push/push-to-start-tokens` | `X-API-Key: sp_live_*` | `registerPushToStartToken()` |
796
+ | DELETE | `/api/push/live-activity-tokens` | `X-API-Key: sp_live_*` | `endLiveActivity()` |
797
+ | POST | `/api/push/send` | `Authorization: Bearer sp_push_*` | (server-side only) |
798
+ | POST | `/api/push/live-activity` | `Authorization: Bearer sp_push_*` | (server-side only) |
799
+
800
+ Default ingest endpoint: `https://ingest.sitepong.com`. Override via the `endpoint` option in `initRN()` or `SitePongRNProvider`.
801
+
599
802
  ### Device Intelligence (React Native)
600
803
 
601
804
  Persistent device identification that **survives app reinstalls**. Requires the `@sitepong/device-id` native module.
@@ -3160,7 +3160,20 @@ var RNPerformanceManager = class {
3160
3160
  };
3161
3161
 
3162
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
+ }
3163
3175
  var cachedDeviceContext = null;
3176
+ var registeredPushToken = null;
3164
3177
  var registeredLiveActivityTokens = /* @__PURE__ */ new Map();
3165
3178
  var registeredPushToStartTokens = /* @__PURE__ */ new Map();
3166
3179
  function getEndpoint() {
@@ -3241,9 +3254,10 @@ async function deleteFromIngest(path, body) {
3241
3254
  }
3242
3255
  }
3243
3256
  function registerDeviceToken(token, options) {
3257
+ ensureIdentifyHookInstalled();
3244
3258
  const device = getDeviceContext();
3245
3259
  const tokenType = options.platform === "ios" ? "apns" : "fcm";
3246
- ({ token, environment: options.environment });
3260
+ registeredPushToken = { token, environment: options.environment };
3247
3261
  postToIngest("/api/push/tokens", {
3248
3262
  native_device_token: token,
3249
3263
  token_type: tokenType,
@@ -3257,6 +3271,7 @@ function registerDeviceToken(token, options) {
3257
3271
  });
3258
3272
  }
3259
3273
  function registerLiveActivityToken(activityType, activityId, pushToken, options) {
3274
+ ensureIdentifyHookInstalled();
3260
3275
  const device = getDeviceContext();
3261
3276
  const userId = getUserId();
3262
3277
  const key = `${activityType}:${activityId}`;
@@ -3276,6 +3291,7 @@ function registerLiveActivityToken(activityType, activityId, pushToken, options)
3276
3291
  });
3277
3292
  }
3278
3293
  function registerPushToStartToken(activityType, pushToStartToken, options) {
3294
+ ensureIdentifyHookInstalled();
3279
3295
  const device = getDeviceContext();
3280
3296
  const userId = getUserId();
3281
3297
  registeredPushToStartTokens.set(activityType, {
@@ -3299,6 +3315,42 @@ function endLiveActivity(activityType, activityId) {
3299
3315
  activity_id: activityId
3300
3316
  });
3301
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
+ }
3302
3354
  var SitePongRNContext = react.createContext({
3303
3355
  isInitialized: false,
3304
3356
  performanceManager: null