@octovise/react-native-sdk 1.0.0

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +160 -0
  4. package/android/src/main/java/com/octovise/sdk/OctoFirebaseMessagingService.kt +72 -0
  5. package/android/src/main/java/com/octovise/sdk/OctoPushDismissReceiver.kt +54 -0
  6. package/android/src/main/java/com/octovise/sdk/OctoPushModule.kt +26 -0
  7. package/android/src/main/java/com/octovise/sdk/OctoPushNotificationBuilder.kt +142 -0
  8. package/dist/api.d.ts +38 -0
  9. package/dist/api.js +210 -0
  10. package/dist/carouselStart.d.ts +1 -0
  11. package/dist/carouselStart.js +6 -0
  12. package/dist/components/OctoErrorBoundary.d.ts +15 -0
  13. package/dist/components/OctoErrorBoundary.js +28 -0
  14. package/dist/components/OctoImage.d.ts +23 -0
  15. package/dist/components/OctoImage.js +69 -0
  16. package/dist/components/OctoMessage.d.ts +9 -0
  17. package/dist/components/OctoMessage.js +189 -0
  18. package/dist/components/OctoModal.d.ts +13 -0
  19. package/dist/components/OctoModal.js +179 -0
  20. package/dist/components/OctoProvider.d.ts +14 -0
  21. package/dist/components/OctoProvider.js +92 -0
  22. package/dist/components/OctoToast.d.ts +13 -0
  23. package/dist/components/OctoToast.js +392 -0
  24. package/dist/debug.d.ts +2 -0
  25. package/dist/debug.js +15 -0
  26. package/dist/imageFit.d.ts +8 -0
  27. package/dist/imageFit.js +31 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/index.js +195 -0
  30. package/dist/latency.d.ts +17 -0
  31. package/dist/latency.js +11 -0
  32. package/dist/native.d.ts +1 -0
  33. package/dist/native.js +14 -0
  34. package/dist/navigation/reactNavigation.d.ts +12 -0
  35. package/dist/navigation/reactNavigation.js +24 -0
  36. package/dist/navigation/resolveCTA.d.ts +12 -0
  37. package/dist/navigation/resolveCTA.js +74 -0
  38. package/dist/push.d.ts +3 -0
  39. package/dist/push.js +129 -0
  40. package/dist/safeNavUrl.d.ts +2 -0
  41. package/dist/safeNavUrl.js +18 -0
  42. package/dist/screenStream.d.ts +17 -0
  43. package/dist/screenStream.js +126 -0
  44. package/dist/screenTargeting.d.ts +37 -0
  45. package/dist/screenTargeting.js +154 -0
  46. package/dist/session.d.ts +2 -0
  47. package/dist/session.js +22 -0
  48. package/dist/state.d.ts +8 -0
  49. package/dist/state.js +53 -0
  50. package/dist/telemetry.d.ts +6 -0
  51. package/dist/telemetry.js +65 -0
  52. package/dist/toastLayout.d.ts +2 -0
  53. package/dist/toastLayout.js +11 -0
  54. package/dist/types.d.ts +78 -0
  55. package/dist/types.js +7 -0
  56. package/dist/version.d.ts +1 -0
  57. package/dist/version.js +4 -0
  58. package/ios/OctoviseNotificationExtension/NotificationService.swift +128 -0
  59. package/ios/OctoviseSDK/OctoPushModule.m +6 -0
  60. package/ios/OctoviseSDK/OctoPushModule.swift +27 -0
  61. package/package.json +57 -0
  62. package/react-native.config.js +16 -0
package/dist/state.js ADDED
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.canInit = canInit;
4
+ exports.markInit = markInit;
5
+ exports.isDuplicate = isDuplicate;
6
+ exports.setCurrentMessage = setCurrentMessage;
7
+ exports.clearCurrentMessage = clearCurrentMessage;
8
+ exports.startForegroundListener = startForegroundListener;
9
+ exports.stopForegroundListener = stopForegroundListener;
10
+ exports.reset = reset;
11
+ const react_native_1 = require("react-native");
12
+ const INIT_COOLDOWN_MS = 90 * 1000;
13
+ let lastInitAt = 0;
14
+ let lastShownMessageId = null;
15
+ let appStateSubscription = null;
16
+ function canInit() {
17
+ if (!lastInitAt)
18
+ return true;
19
+ return Date.now() - lastInitAt >= INIT_COOLDOWN_MS;
20
+ }
21
+ function markInit() {
22
+ lastInitAt = Date.now();
23
+ }
24
+ function isDuplicate(messageId) {
25
+ if (!messageId)
26
+ return false;
27
+ return messageId === lastShownMessageId;
28
+ }
29
+ function setCurrentMessage(messageId) {
30
+ lastShownMessageId = messageId ?? null;
31
+ }
32
+ function clearCurrentMessage() {
33
+ lastShownMessageId = null;
34
+ }
35
+ function startForegroundListener(onForeground) {
36
+ if (appStateSubscription)
37
+ return;
38
+ appStateSubscription = react_native_1.AppState.addEventListener('change', (nextState) => {
39
+ if (nextState === 'active' && canInit()) {
40
+ markInit();
41
+ onForeground();
42
+ }
43
+ });
44
+ }
45
+ function stopForegroundListener() {
46
+ appStateSubscription?.remove();
47
+ appStateSubscription = null;
48
+ }
49
+ function reset() {
50
+ lastInitAt = 0;
51
+ lastShownMessageId = null;
52
+ stopForegroundListener();
53
+ }
@@ -0,0 +1,6 @@
1
+ export declare function inMetricsSample(uid: number | string | null, digits: number[]): boolean;
2
+ export declare function configureTelemetry(uid: number | string): void;
3
+ export declare function setMetricSampleDigits(digits: unknown): void;
4
+ export declare function reportError(name: string, meta?: Record<string, unknown>): void;
5
+ export declare function reportMetric(name: string, value: number, meta?: Record<string, unknown>): void;
6
+ export declare function __resetTelemetryForTests(): void;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inMetricsSample = inMetricsSample;
4
+ exports.configureTelemetry = configureTelemetry;
5
+ exports.setMetricSampleDigits = setMetricSampleDigits;
6
+ exports.reportError = reportError;
7
+ exports.reportMetric = reportMetric;
8
+ exports.__resetTelemetryForTests = __resetTelemetryForTests;
9
+ const api_1 = require("./api");
10
+ let sampleDigits = [];
11
+ let userId = null;
12
+ const queue = [];
13
+ let flushTimer = null;
14
+ const FLUSH_MS = 2000;
15
+ const FLUSH_SIZE = 20;
16
+ function inMetricsSample(uid, digits) {
17
+ if (uid == null || digits.length === 0)
18
+ return false;
19
+ const n = Number(uid);
20
+ if (!Number.isFinite(n))
21
+ return false;
22
+ return digits.includes(Math.abs(Math.trunc(n)) % 10);
23
+ }
24
+ function configureTelemetry(uid) {
25
+ userId = uid;
26
+ }
27
+ function setMetricSampleDigits(digits) {
28
+ sampleDigits = Array.isArray(digits) ? digits.filter((d) => typeof d === 'number') : [];
29
+ }
30
+ function reportError(name, meta) {
31
+ enqueue({ kind: 'error', name, meta });
32
+ }
33
+ function reportMetric(name, value, meta) {
34
+ if (!inMetricsSample(userId, sampleDigits))
35
+ return;
36
+ enqueue({ kind: 'metric', name, value, meta });
37
+ }
38
+ function enqueue(e) {
39
+ queue.push(e);
40
+ if (queue.length >= FLUSH_SIZE) {
41
+ flush();
42
+ return;
43
+ }
44
+ if (!flushTimer)
45
+ flushTimer = setTimeout(flush, FLUSH_MS);
46
+ }
47
+ function flush() {
48
+ if (flushTimer) {
49
+ clearTimeout(flushTimer);
50
+ flushTimer = null;
51
+ }
52
+ if (queue.length === 0)
53
+ return;
54
+ const events = queue.splice(0, queue.length);
55
+ (0, api_1.sendTelemetry)(events);
56
+ }
57
+ function __resetTelemetryForTests() {
58
+ sampleDigits = [];
59
+ userId = null;
60
+ queue.length = 0;
61
+ if (flushTimer) {
62
+ clearTimeout(flushTimer);
63
+ flushTimer = null;
64
+ }
65
+ }
@@ -0,0 +1,2 @@
1
+ export type ToastLayout = 'imageOnly' | 'textOnly' | 'stacked' | 'carousel';
2
+ export declare function toastLayout(hasImage: boolean, hasText: boolean, imagePosition?: string): ToastLayout;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toastLayout = toastLayout;
4
+ function toastLayout(hasImage, hasText, imagePosition) {
5
+ if (hasImage && !hasText)
6
+ return 'imageOnly';
7
+ if (!hasImage)
8
+ return 'textOnly';
9
+ const pos = imagePosition || 'left';
10
+ return pos === 'top' || pos === 'bottom' ? 'stacked' : 'carousel';
11
+ }
@@ -0,0 +1,78 @@
1
+ export interface NavAdapter {
2
+ navigate: (routeName: string, params?: Record<string, string>) => void;
3
+ getCurrentScreen?: () => string | null;
4
+ subscribeScreenChange?: (cb: (screenName: string, params?: Record<string, unknown>) => void) => () => void;
5
+ }
6
+ export interface OctoRNConfig {
7
+ apiKey: string;
8
+ userId: number | string;
9
+ userHash?: string;
10
+ baseUrl?: string;
11
+ appVersion?: string;
12
+ onCTA?: (url: string) => void;
13
+ nav?: NavAdapter;
14
+ onEvent?: (event: EventType, meta: Partial<EventMeta>, sendId: number) => void;
15
+ registerPushToken?: boolean;
16
+ }
17
+ export interface EntitySignal {
18
+ type: string;
19
+ id?: number | null;
20
+ ref?: string | null;
21
+ }
22
+ export interface InAppMessage {
23
+ id?: string;
24
+ _send_id?: number;
25
+ __timing?: {
26
+ t0: number;
27
+ tCheck: number;
28
+ };
29
+ layout: 'modal' | 'toast';
30
+ position?: 'center' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
31
+ mobile_position?: 'top' | 'center' | 'bottom';
32
+ size?: 'small' | 'medium' | 'large';
33
+ text_size?: 'small' | 'medium' | 'large';
34
+ text_align?: 'left' | 'center' | 'right';
35
+ title?: string;
36
+ body?: string;
37
+ image?: string;
38
+ image_position?: 'left' | 'right' | 'top' | 'bottom';
39
+ image_fit?: 'cover' | 'contain';
40
+ image_bg?: string;
41
+ cta: {
42
+ label: string;
43
+ url: string;
44
+ align?: 'left' | 'center' | 'right';
45
+ devices?: ('web' | 'mobile')[];
46
+ };
47
+ click_mode?: 'cta' | 'full' | 'none';
48
+ dismiss_label?: string;
49
+ colors?: {
50
+ primary?: string;
51
+ background?: string;
52
+ text?: string;
53
+ };
54
+ }
55
+ export type EventType = 'impression' | 'click' | 'dismiss' | 'received';
56
+ export interface EventMeta {
57
+ duration_ms: number;
58
+ time_to_first_interaction_ms: number | null;
59
+ close_method?: 'cta' | 'dismiss_button' | 'backdrop' | 'close_button';
60
+ device: 'mobile';
61
+ platform: 'ios' | 'android';
62
+ image_loaded: boolean;
63
+ latency?: import('./latency').LatencyBreakdown;
64
+ image_url?: string;
65
+ }
66
+ export interface PushPayload {
67
+ source: 'octovise';
68
+ send_id: string;
69
+ campaign_id: string;
70
+ title: string;
71
+ body: string;
72
+ image_url?: string;
73
+ image_layout?: 'inline' | 'hero';
74
+ deep_link?: string;
75
+ channel_id?: string;
76
+ collapse_key?: string;
77
+ }
78
+ export declare function parsePushSendId(raw: unknown): number;
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parsePushSendId = parsePushSendId;
4
+ function parsePushSendId(raw) {
5
+ const n = Number(raw);
6
+ return Number.isFinite(n) && n > 0 ? n : 0;
7
+ }
@@ -0,0 +1 @@
1
+ export declare const SDK_VERSION = "1.0.0";
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SDK_VERSION = void 0;
4
+ exports.SDK_VERSION = '1.0.0';
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Notification Service Extension for Octovise push notifications on iOS.
3
+ *
4
+ * This runs in a separate process (even when the app is killed) and has
5
+ * ~30 seconds to modify the notification before iOS displays it.
6
+ *
7
+ * Responsibilities:
8
+ * - Download the image and attach it as a media attachment
9
+ * - Track "received" event via POST /event
10
+ * - Only processes notifications where data.source == "octovise"
11
+ *
12
+ * Setup: the client adds this extension target to their Xcode project and
13
+ * sets the App Group so it can read the SDK config from shared UserDefaults.
14
+ */
15
+
16
+ import UserNotifications
17
+
18
+ class NotificationService: UNNotificationServiceExtension {
19
+
20
+ private var contentHandler: ((UNNotificationContent) -> Void)?
21
+ private var bestAttemptContent: UNMutableNotificationContent?
22
+
23
+ override func didReceive(
24
+ _ request: UNNotificationRequest,
25
+ withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
26
+ ) {
27
+ self.contentHandler = contentHandler
28
+ bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent
29
+
30
+ guard let content = bestAttemptContent,
31
+ let source = request.content.userInfo["source"] as? String,
32
+ source == "octovise" else {
33
+ // Not ours — pass through unmodified
34
+ contentHandler(request.content)
35
+ return
36
+ }
37
+
38
+ let userInfo = request.content.userInfo
39
+
40
+ // Track received (fire-and-forget)
41
+ trackReceived(userInfo: userInfo)
42
+
43
+ // Download and attach image
44
+ guard let imageUrlString = userInfo["image_url"] as? String,
45
+ let imageUrl = URL(string: imageUrlString) else {
46
+ contentHandler(content)
47
+ return
48
+ }
49
+
50
+ downloadImage(from: imageUrl) { attachment in
51
+ if let attachment = attachment {
52
+ content.attachments = [attachment]
53
+ }
54
+ contentHandler(content)
55
+ }
56
+ }
57
+
58
+ override func serviceExtensionTimeWillExpire() {
59
+ // iOS is about to kill us — deliver what we have
60
+ if let content = bestAttemptContent {
61
+ contentHandler?(content)
62
+ }
63
+ }
64
+
65
+ // MARK: - Image Download
66
+
67
+ private func downloadImage(from url: URL, completion: @escaping (UNNotificationAttachment?) -> Void) {
68
+ let task = URLSession.shared.downloadTask(with: url) { localUrl, response, error in
69
+ guard let localUrl = localUrl, error == nil else {
70
+ completion(nil)
71
+ return
72
+ }
73
+
74
+ // Move to a temp file with proper extension
75
+ let ext = url.pathExtension.isEmpty ? "jpg" : url.pathExtension
76
+ let tempUrl = FileManager.default.temporaryDirectory
77
+ .appendingPathComponent(UUID().uuidString)
78
+ .appendingPathExtension(ext)
79
+
80
+ do {
81
+ try FileManager.default.moveItem(at: localUrl, to: tempUrl)
82
+ let attachment = try UNNotificationAttachment(identifier: "octovise-image", url: tempUrl)
83
+ completion(attachment)
84
+ } catch {
85
+ completion(nil)
86
+ }
87
+ }
88
+ task.resume()
89
+ }
90
+
91
+ // MARK: - Event Tracking
92
+
93
+ private func trackReceived(userInfo: [AnyHashable: Any]) {
94
+ // Read SDK config from shared App Group UserDefaults
95
+ guard let defaults = UserDefaults(suiteName: "group.octovise.sdk"),
96
+ let baseUrl = defaults.string(forKey: "base_url"),
97
+ let apiKey = defaults.string(forKey: "api_key"),
98
+ let userId = defaults.string(forKey: "user_id"),
99
+ let sendIdStr = userInfo["send_id"] as? String,
100
+ let sendId = Int(sendIdStr) else {
101
+ return
102
+ }
103
+
104
+ let payload: [String: Any] = [
105
+ "user_id": userId,
106
+ "send_id": sendId,
107
+ "event": "received",
108
+ "meta": [
109
+ "device": "mobile",
110
+ "platform": "ios",
111
+ ],
112
+ ]
113
+
114
+ guard let url = URL(string: "\(baseUrl)/event"),
115
+ let body = try? JSONSerialization.data(withJSONObject: payload) else {
116
+ return
117
+ }
118
+
119
+ var request = URLRequest(url: url)
120
+ request.httpMethod = "POST"
121
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
122
+ request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
123
+ request.httpBody = body
124
+ request.timeoutInterval = 5
125
+
126
+ URLSession.shared.dataTask(with: request).resume()
127
+ }
128
+ }
@@ -0,0 +1,6 @@
1
+ // Objective-C bridge for RN to find the Swift module
2
+ #import <React/RCTBridgeModule.h>
3
+
4
+ @interface RCT_EXTERN_MODULE(OctoPushModule, NSObject)
5
+ RCT_EXTERN_METHOD(configure:(NSString *)apiKey userId:(NSString *)userId baseUrl:(NSString *)baseUrl)
6
+ @end
@@ -0,0 +1,27 @@
1
+ /**
2
+ * React Native bridge module for iOS.
3
+ * Persists SDK config to shared App Group UserDefaults so the
4
+ * Notification Service Extension can access them.
5
+ */
6
+
7
+ import Foundation
8
+
9
+ @objc(OctoPushModule)
10
+ class OctoPushModule: NSObject {
11
+
12
+ @objc
13
+ func configure(_ apiKey: String, userId: String, baseUrl: String) {
14
+ guard let defaults = UserDefaults(suiteName: "group.octovise.sdk") else {
15
+ return
16
+ }
17
+ defaults.set(apiKey, forKey: "api_key")
18
+ defaults.set(userId, forKey: "user_id")
19
+ defaults.set(baseUrl, forKey: "base_url")
20
+ defaults.synchronize()
21
+ }
22
+
23
+ @objc
24
+ static func requiresMainQueueSetup() -> Bool {
25
+ return false
26
+ }
27
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@octovise/react-native-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Octovise SDK for React Native — in-app messaging (push notifications in v1.1)",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "jest",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "peerDependencies": {
16
+ "@react-native-firebase/messaging": ">=18",
17
+ "react": ">=18",
18
+ "react-native": ">=0.72",
19
+ "react-native-safe-area-context": ">=4"
20
+ },
21
+ "peerDependenciesMeta": {
22
+ "@react-native-firebase/messaging": {
23
+ "optional": true
24
+ }
25
+ },
26
+ "devDependencies": {
27
+ "@testing-library/react-native": "^12.9.0",
28
+ "@types/jest": "^29.5.12",
29
+ "@types/react": "^18",
30
+ "jest": "^29.7.0",
31
+ "react": "^18",
32
+ "react-native": "^0.74",
33
+ "react-native-safe-area-context": "^5.8.0",
34
+ "react-test-renderer": "^18.2.0",
35
+ "ts-jest": "^29.1.5",
36
+ "typescript": "^5.7.0"
37
+ },
38
+ "files": [
39
+ "dist/",
40
+ "android/",
41
+ "ios/",
42
+ "react-native.config.js",
43
+ "CHANGELOG.md"
44
+ ],
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/maurogallardo-cell/octovise-react-native-sdk.git"
48
+ },
49
+ "license": "MIT",
50
+ "keywords": [
51
+ "react-native",
52
+ "in-app-messaging",
53
+ "push-notifications",
54
+ "octovise",
55
+ "sdk"
56
+ ]
57
+ }
@@ -0,0 +1,16 @@
1
+ // Tell RN autolinking to skip the native (Android/iOS) modules for now.
2
+ // The Kotlin/Swift sources in android/ and ios/ are intended to be built
3
+ // as a proper RN native module in v2 (with build.gradle + podspec).
4
+ //
5
+ // For v1, the SDK is JS-only: in-app messaging works via fetch() + Modal/Animated.
6
+ // Push notifications (which need the native modules) are disabled by default.
7
+ // Setting platforms to null prevents autolinking from trying to compile
8
+ // the Kotlin/Swift sources without a proper module setup.
9
+ module.exports = {
10
+ dependency: {
11
+ platforms: {
12
+ android: null,
13
+ ios: null,
14
+ },
15
+ },
16
+ }