diff --git a/.gitignore b/.gitignore index 04fb8de..1127eb8 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ examples/.expo/ # OS .DS_Store + +# CocoaPods (library — do not commit generated files) +ios/Podfile.lock +ios/*.xcworkspace/ diff --git a/examples/App.tsx b/examples/App.tsx index 3fea469..62cca38 100644 --- a/examples/App.tsx +++ b/examples/App.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { SafeAreaView, StyleSheet } from 'react-native'; import { StatusBar } from 'expo-status-bar'; import { UserbackProvider, UserbackSDK } from '@userback/react-native-sdk'; @@ -10,24 +10,41 @@ import ObserversScreen from './screens/ObserversScreen'; export type Screen = 'home' | 'basic' | 'auth' | 'advanced' | 'observers'; +const SCREEN_NAMES: Record = { + home: 'HomeScreen', + basic: 'BasicScreen', + auth: 'AuthFlowScreen', + advanced: 'AdvancedScreen', + observers: 'ObserversScreen', +}; + export default function App() { const [screen, setScreen] = useState('home'); + const prevScreen = useRef(null); useEffect(() => { // Start once at the app root — all screens share this instance UserbackSDK.start({ accessToken: 'YOUR_ACCESS_TOKEN', userData: { - id: 'user-123', + id: "user-123", // example data info: { name: 'Jane Doe', email: 'jane@example.com', - }, - }, + } + } }); return () => UserbackSDK.stop(); }, []); + useEffect(() => { + if (prevScreen.current) { + UserbackSDK.leaveScreen(SCREEN_NAMES[prevScreen.current]); + } + UserbackSDK.enterScreen(SCREEN_NAMES[screen]); + prevScreen.current = screen; + }, [screen]); + return ( diff --git a/examples/screens/AdvancedScreen.tsx b/examples/screens/AdvancedScreen.tsx index 297a888..bbb441c 100644 --- a/examples/screens/AdvancedScreen.tsx +++ b/examples/screens/AdvancedScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { UserbackSDK } from '@userback/react-native-sdk'; @@ -9,6 +9,11 @@ interface Props { export default function AdvancedScreen({ goBack }: Props) { const [replayActive, setReplayActive] = useState(false); + useEffect(() => { + UserbackSDK.enterScreen('AdvancedScreen'); + return () => { UserbackSDK.leaveScreen('AdvancedScreen'); }; + }, []); + function toggleReplay() { if (replayActive) { UserbackSDK.stopSessionReplay(); @@ -25,20 +30,33 @@ export default function AdvancedScreen({ goBack }: Props) { Feedback Modes - UserbackSDK.openForm('bug')}> + UserbackSDK.openForm('', 'bug')}> Bug - UserbackSDK.openForm('feature_request')}> + UserbackSDK.openForm('', 'feature_request')}> Feature - UserbackSDK.openForm('general')}> + UserbackSDK.openForm('', 'general')}> General + Multiple Projects + + UserbackSDK.openForm('YOUR_PROJECT_KEY_1')}> + Project 1 + + UserbackSDK.openForm('YOUR_PROJECT_KEY_2')}> + Project 2 + + UserbackSDK.openForm('YOUR_PROJECT_KEY_3')}> + Project 3 + + + Attach Metadata UserbackSDK.setData({ screen: 'AdvancedScreen', @@ -50,6 +68,22 @@ export default function AdvancedScreen({ goBack }: Props) { setData (screen + version) + Custom Events + + UserbackSDK.addCustomEvent('button_clicked', { screen: 'AdvancedScreen' })} + > + button_clicked + + UserbackSDK.addCustomEvent('purchase_completed', { amount: 99, currency: 'USD' })} + > + purchase_completed + + + Session Replay { + UserbackSDK.enterScreen('BasicScreen'); + return () => { UserbackSDK.leaveScreen('BasicScreen'); }; + }, []); + return ( Basic Usage @@ -14,10 +19,14 @@ export default function BasicScreen({ goBack }: Props) { UserbackProvider and start() live in App.tsx. Any screen can call openForm(). - UserbackSDK.openForm()}> + UserbackSDK.openForm('', 'general')}> Open Feedback + UserbackSDK.openSurvey('YOUR_SURVEY_KEY')}> + Open Survey + + ← Back @@ -37,6 +46,7 @@ const styles = StyleSheet.create({ marginBottom: 12, }, buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' }, + surveyButton: { backgroundColor: '#43A047' }, secondary: { backgroundColor: 'transparent' }, secondaryText: { color: '#5C6BC0', fontSize: 16 }, }); diff --git a/ios/userbackreactnativesdk.xcodeproj/project.pbxproj b/ios/userbackreactnativesdk.xcodeproj/project.pbxproj index 5240613..108a2ee 100644 --- a/ios/userbackreactnativesdk.xcodeproj/project.pbxproj +++ b/ios/userbackreactnativesdk.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; + 5603264EB79201FDD27E5D89 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F551294207C1F9213E21FEF0 /* PrivacyInfo.xcprivacy */; }; 96905EF65AED1B983A6B3ABC /* libPods-userbackreactnativesdk.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-userbackreactnativesdk.a */; }; B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; }; BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; @@ -29,6 +30,7 @@ AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = userbackreactnativesdk/SplashScreen.storyboard; sourceTree = ""; }; BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + F551294207C1F9213E21FEF0 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = userbackreactnativesdk/PrivacyInfo.xcprivacy; sourceTree = ""; }; FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-userbackreactnativesdk/ExpoModulesProvider.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -54,6 +56,7 @@ 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB71A68108700A75B9A /* main.m */, AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, + F551294207C1F9213E21FEF0 /* PrivacyInfo.xcprivacy */, ); name = userbackreactnativesdk; sourceTree = ""; @@ -139,11 +142,13 @@ buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "userbackreactnativesdk" */; buildPhases = ( 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */, + 992313AB5608F920D9BC956C /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */, + 9CEBF89FC0445DF638994C66 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -193,6 +198,7 @@ BB2F792D24A3F905000567C9 /* Expo.plist in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, + 5603264EB79201FDD27E5D89 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -244,20 +250,71 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-userbackreactnativesdk/Pods-userbackreactnativesdk-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevLauncher.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-userbackreactnativesdk/Pods-userbackreactnativesdk-resources.sh\"\n"; showEnvVarsInLog = 0; }; + 992313AB5608F920D9BC956C /* [Expo] Configure project */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "[Expo] Configure project"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-userbackreactnativesdk/expo-configure-project.sh\"\n"; + }; + 9CEBF89FC0445DF638994C66 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-userbackreactnativesdk/Pods-userbackreactnativesdk-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-userbackreactnativesdk/Pods-userbackreactnativesdk-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -295,6 +352,7 @@ "-ObjC", "-lc++", ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = org.name.userbackreactnativesdk; PRODUCT_NAME = userbackreactnativesdk; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -319,6 +377,7 @@ "-ObjC", "-lc++", ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = org.name.userbackreactnativesdk; PRODUCT_NAME = userbackreactnativesdk; SWIFT_VERSION = 5.0; @@ -375,10 +434,17 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; - LIBRARY_SEARCH_PATHS = "\"$(inherited)\""; + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; + USE_HERMES = true; }; name = Debug; }; @@ -424,9 +490,15 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; - LIBRARY_SEARCH_PATHS = "\"$(inherited)\""; + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; + USE_HERMES = true; VALIDATE_PRODUCT = YES; }; name = Release; diff --git a/ios/userbackreactnativesdk/PrivacyInfo.xcprivacy b/ios/userbackreactnativesdk/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..5bb83c5 --- /dev/null +++ b/ios/userbackreactnativesdk/PrivacyInfo.xcprivacy @@ -0,0 +1,48 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + 0A2A.1 + 3B52.1 + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + 85F4.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/src/UserbackProvider.tsx b/src/UserbackProvider.tsx index 9ed74a8..f0b457c 100644 --- a/src/UserbackProvider.tsx +++ b/src/UserbackProvider.tsx @@ -95,6 +95,55 @@ function buildHTML(config: UserbackConfig): string { `; } +type SurveyLayoutConfig = { + format: string; + position: string; + size: string; + hasOverlay: boolean; +}; + +type SurveyInfo = SurveyLayoutConfig & { height: number }; + +const SURVEY_SPACE = 24; + +const SURVEY_SIZE_WIDTHS: Record = { + 'smaller': 352, 'smaller-wide': 448, + 'small': 448, 'small-wide': 544, + 'medium': 544, 'medium-wide': 640, + 'large': 640, 'large-wide': 736, + 'larger': 736, 'larger-wide': 832, + 'largest': 1120, +}; + +function getSurveyContainerStyle(info: SurveyInfo): object { + if (info.hasOverlay) { + return StyleSheet.absoluteFillObject; + } + + const screen = Dimensions.get('window'); + const width = Math.min(SURVEY_SIZE_WIDTHS[info.size] ?? 640, screen.width - SURVEY_SPACE * 2); + const height = info.height; + const centerX = (screen.width - width) / 2; + const centerY = (screen.height - height) / 2; + + if (info.format === 'pageless') { + return { position: 'absolute' as const, top: 0, left: centerX, width, height: screen.height }; + } + + switch (info.position) { + case 'top': return { position: 'absolute' as const, top: SURVEY_SPACE, left: centerX, width, height }; + case 'top_left': return { position: 'absolute' as const, top: SURVEY_SPACE, left: SURVEY_SPACE, width, height }; + case 'top_right': return { position: 'absolute' as const, top: SURVEY_SPACE, right: SURVEY_SPACE, width, height }; + case 'bottom': return { position: 'absolute' as const, bottom: SURVEY_SPACE, left: centerX, width, height }; + case 'bottom_left': return { position: 'absolute' as const, bottom: SURVEY_SPACE, left: SURVEY_SPACE, width, height }; + case 'bottom_right':return { position: 'absolute' as const, bottom: SURVEY_SPACE, right: SURVEY_SPACE, width, height }; + case 'left': return { position: 'absolute' as const, top: centerY, left: SURVEY_SPACE, width, height }; + case 'right': return { position: 'absolute' as const, top: centerY, right: SURVEY_SPACE, width, height }; + case 'center': return { position: 'absolute' as const, top: centerY, left: centerX, width, height }; + default: return StyleSheet.absoluteFillObject; + } +} + interface UserbackProviderProps { children: React.ReactNode; } @@ -102,7 +151,9 @@ interface UserbackProviderProps { export function UserbackProvider({ children }: UserbackProviderProps) { const [config, setConfig] = useState(null); const [widgetOpen, setWidgetOpen] = useState(false); + const [surveyInfo, setSurveyInfo] = useState(null); const [takingScreenshot, setTakingScreenshot] = useState(false); + const surveyConfigsRef = useRef>({}); const webViewRef = useRef(null); useEffect(() => { @@ -163,12 +214,32 @@ export function UserbackProvider({ children }: UserbackProviderProps) { const handleMessage = useCallback((event: WebViewMessageEvent) => { try { const data = JSON.parse(event.nativeEvent.data); + const type = (data.type ?? data.event ?? '').toLowerCase(); // Mirror iOS SDK: widget_resize with last:true → show, close → hide if (type === 'widget_resize' && data.payload?.last === true) setWidgetOpen(true); if (type === 'close') setWidgetOpen(false); + if (type === 'survey_configs' && Array.isArray(data.payload)) { + const map: Record = {}; + for (const cfg of data.payload) { + if (cfg.key) map[cfg.key] = { format: cfg.format ?? '', position: cfg.position ?? 'center', size: cfg.size ?? 'large', hasOverlay: !!cfg.has_background_colour }; + } + surveyConfigsRef.current = map; + } + if (type === 'survey_open') { + if (!widgetOpen) { + const cfg = data.payload?.key ? surveyConfigsRef.current[data.payload.key] : undefined; + setSurveyInfo(cfg ? { ...cfg, height: 0 } : { format: '', position: 'center', size: 'large', hasOverlay: false, height: 0 }); + } + } + if (type === 'survey_close') setSurveyInfo(null); + if (type === 'survey_height') { + const { height } = data.payload ?? {}; + if (height) setSurveyInfo(prev => prev ? { ...prev, height: height + 40 } : null); + } + UserbackSDK._onMessage(data); } catch (e) { if (__DEV__) console.warn('[Userback] failed to parse message:', e); @@ -179,14 +250,13 @@ export function UserbackProvider({ children }: UserbackProviderProps) { const baseUrl = (() => { try { return new URL(widgetJSURL).origin; } catch { return 'https://static.userback.io'; } })(); - return ( <> {children} {config && ( - + ): void { const type = (data.type ?? data.event ?? '').toLowerCase(); switch (type) { - case 'load': + case 'load': { this._onReady(); - if (data.payload) { - this._widgetConfig = data.payload; - this.onWidgetConfigLoaded?.(data.payload); - this._startObservers(data.payload); + const feedbackConfig = data.payload?.feedback ?? data.payload; + if (feedbackConfig) { + this._widgetConfig = feedbackConfig; + this.onWidgetConfigLoaded?.(feedbackConfig); + this._startObservers(feedbackConfig); } break; + } case 'widget_resize': this._clearFormOpenTimeout(); @@ -269,8 +271,9 @@ class UserbackSDKClass extends Emitter { this._run('destroy', [keepInstance, keepRecorder]); } - openForm(mode = '', directTo?: string): void { + openForm(projectKey = '', mode = '', directTo?: string): void { this._clearFormOpenTimeout(); + this._run('setActiveWidget', [projectKey]); this._run('openForm', [mode, directTo ?? null]); this._formOpenTimeout = setTimeout(() => { if (__DEV__) console.log('[Userback] openForm timed out — widget did not respond.'); @@ -282,6 +285,7 @@ class UserbackSDKClass extends Emitter { openPortal(): void { this._run('openPortal'); } openRoadmap(): void { this._run('openRoadmap'); } openAnnouncement(): void { this._run('openAnnouncement'); } + openSurvey(surveyKey: string): void { this._run('openSurvey', [surveyKey]); } close(): void { this._run('close'); } setEmail(email: string): void { this._run('setEmail', [email]); } @@ -307,6 +311,26 @@ class UserbackSDKClass extends Emitter { addCustomEvent(title: string, details?: Record): void { this._run('addCustomEvent', [title, details ?? null]); } + + enterScreen(name: string): void { + const detail = { screenName: name, action: 'enter' }; + const js = `(function(){window.dispatchEvent(new CustomEvent('userback:nativeScreen',{detail:${JSON.stringify(detail)}}));})();true;`; + if (this._ready && this._inject) { + this._inject(js); + } else { + this._pending.push(js); + } + } + + leaveScreen(name: string): void { + const detail = { screenName: name, action: 'leave' }; + const js = `(function(){window.dispatchEvent(new CustomEvent('userback:nativeScreen',{detail:${JSON.stringify(detail)}}));})();true;`; + if (this._ready && this._inject) { + this._inject(js); + } else { + this._pending.push(js); + } + } } export const UserbackSDK = new UserbackSDKClass(); diff --git a/tsconfig.json b/tsconfig.json index 700ee74..de69cf0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,12 +3,17 @@ "target": "ES2017", "module": "ESNext", "moduleResolution": "Bundler", - "lib": ["ES2017"], + "lib": [ + "ES2017" + ], "jsx": "react-native", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "noEmit": true }, - "include": ["src"] + "include": [ + "src" + ], + "extends": "expo/tsconfig.base" }