diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..e4c74bf
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,28 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: yarn
+
+ - name: Install dependencies
+ run: yarn install --immutable
+
+ - name: Type check
+ run: yarn typecheck
+
+ - name: Build
+ run: yarn build
diff --git a/examples/app.json b/examples/app.json
index 61a5300..c0ade5f 100644
--- a/examples/app.json
+++ b/examples/app.json
@@ -3,7 +3,7 @@
"name": "Userback SDK Example",
"slug": "userback-sdk-example",
"version": "1.0.0",
- "orientation": "portrait",
+ "orientation": "default",
"platforms": [
"ios",
"android"
diff --git a/examples/ios/UserbackSDKExample.xcodeproj/project.pbxproj b/examples/ios/UserbackSDKExample.xcodeproj/project.pbxproj
index a7d936a..8a117cb 100644
--- a/examples/ios/UserbackSDKExample.xcodeproj/project.pbxproj
+++ b/examples/ios/UserbackSDKExample.xcodeproj/project.pbxproj
@@ -365,7 +365,7 @@
SWIFT_OBJC_BRIDGING_HEADER = "UserbackSDKExample/UserbackSDKExample-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = 1;
+ TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@@ -392,7 +392,7 @@
PRODUCT_NAME = UserbackSDKExample;
SWIFT_OBJC_BRIDGING_HEADER = "UserbackSDKExample/UserbackSDKExample-Bridging-Header.h";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = 1;
+ TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
diff --git a/examples/ios/UserbackSDKExample/Info.plist b/examples/ios/UserbackSDKExample/Info.plist
index 82c2764..ada64d5 100644
--- a/examples/ios/UserbackSDKExample/Info.plist
+++ b/examples/ios/UserbackSDKExample/Info.plist
@@ -51,13 +51,15 @@
arm64
UIRequiresFullScreen
-
+
UIStatusBarStyle
UIStatusBarStyleDefault
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
UISupportedInterfaceOrientations~ipad
diff --git a/src/UserbackProvider.tsx b/src/UserbackProvider.tsx
index 7b3c35a..9ed74a8 100644
--- a/src/UserbackProvider.tsx
+++ b/src/UserbackProvider.tsx
@@ -108,11 +108,14 @@ export function UserbackProvider({ children }: UserbackProviderProps) {
useEffect(() => {
const onStart = (cfg: UserbackConfig) => setConfig({ ...cfg });
const onStop = () => setConfig(null);
+ const onForceClose = () => setWidgetOpen(false);
UserbackSDK.on('_start', onStart);
UserbackSDK.on('_stop', onStop);
+ UserbackSDK.on('_forceClose', onForceClose);
return () => {
UserbackSDK.off('_start', onStart);
UserbackSDK.off('_stop', onStop);
+ UserbackSDK.off('_forceClose', onForceClose);
};
}, []);
@@ -142,6 +145,13 @@ export function UserbackProvider({ children }: UserbackProviderProps) {
webViewRef.current?.injectJavaScript(js);
}, []);
+ useEffect(() => {
+ const sub = Dimensions.addEventListener('change', () => {
+ inject(`(function(){window.dispatchEvent(new CustomEvent('userback:rotate'));})();true;`);
+ });
+ return () => sub.remove();
+ }, [inject]);
+
useEffect(() => {
if (config) {
UserbackSDK._attach(inject);
diff --git a/src/UserbackSDK.ts b/src/UserbackSDK.ts
index 2d3a980..1d31e2e 100644
--- a/src/UserbackSDK.ts
+++ b/src/UserbackSDK.ts
@@ -36,6 +36,7 @@ class UserbackSDKClass extends Emitter {
private _ready = false;
private _pending: string[] = [];
private _inject: ((js: string) => void) | null = null;
+ private _formOpenTimeout: ReturnType | null = null;
public onWidgetConfigLoaded?: (config: Record) => void;
public onWidgetResize?: (size: { width: number; height: number }) => void;
@@ -105,6 +106,7 @@ class UserbackSDKClass extends Emitter {
break;
case 'widget_resize':
+ this._clearFormOpenTimeout();
this.onWidgetResize?.(data.payload ?? {});
break;
@@ -191,14 +193,21 @@ class UserbackSDKClass extends Emitter {
}
}
+ private _clearFormOpenTimeout(): void {
+ if (this._formOpenTimeout !== null) {
+ clearTimeout(this._formOpenTimeout);
+ this._formOpenTimeout = null;
+ }
+ }
+
private _sendNativeEvent(event: Record): void {
if (!this._inject) return;
if (event.type === 'log') {
- const detail = { payload: { type: event.level, message: event.message } };
+ const detail = { payload: { type: event.level, message: event.message, mobileSDK: true }, mobileSDK: true };
const js = `(function(){window.dispatchEvent(new CustomEvent('userback:nativeLogEvent',{detail:${JSON.stringify(detail)}}));})();true;`;
this._inject(js);
} else {
- const detail = { payload: event };
+ const detail = { payload: { ...event, mobileSDK: true }, mobileSDK: true };
const js = `(function(){window.dispatchEvent(new CustomEvent('userback:nativeNetworkEvent',{detail:${JSON.stringify(detail)}}));})();true;`;
this._inject(js);
}
@@ -206,11 +215,8 @@ class UserbackSDKClass extends Emitter {
/** @internal — called by UserbackProvider after capturing the screenshot */
_sendScreenshot(dataURL: string): void {
- const message = JSON.stringify({
- type: 'native_screenshot',
- payload: { data_url: dataURL },
- });
- const js = `(function(){window.dispatchEvent(new CustomEvent('userback:nativeScreenshot',{detail:${message}}));})();true;`;
+ const detail = { type: 'native_screenshot', payload: { data_url: dataURL, mobileSDK: true }, mobileSDK: true };
+ const js = `(function(){window.dispatchEvent(new CustomEvent('userback:nativeScreenshot',{detail:${JSON.stringify(detail)}}));})();true;`;
if (this._inject) this._inject(js);
}
@@ -234,6 +240,7 @@ class UserbackSDKClass extends Emitter {
}
stop(): void {
+ this._clearFormOpenTimeout();
LogObserver.stop();
NetworkObserver.stop();
this._config = null;
@@ -263,7 +270,13 @@ class UserbackSDKClass extends Emitter {
}
openForm(mode = '', directTo?: string): void {
+ this._clearFormOpenTimeout();
this._run('openForm', [mode, directTo ?? null]);
+ this._formOpenTimeout = setTimeout(() => {
+ if (__DEV__) console.log('[Userback] openForm timed out — widget did not respond.');
+ this._clearFormOpenTimeout();
+ this.emit('_forceClose');
+ }, 5000);
}
openPortal(): void { this._run('openPortal'); }
diff --git a/yarn.lock b/yarn.lock
index ba57f14..b4ffa91 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2186,7 +2186,6 @@ __metadata:
resolution: "@userback/react-native-sdk@workspace:."
dependencies:
"@types/react": "npm:^18.0.0"
- expo: "npm:~52.0.49"
react: "npm:18.3.1"
react-native: "npm:0.76.9"
react-native-view-shot: "npm:^3.8.0"
@@ -3816,7 +3815,7 @@ __metadata:
languageName: node
linkType: hard
-"expo@npm:~52.0.0, expo@npm:~52.0.49":
+"expo@npm:~52.0.0":
version: 52.0.49
resolution: "expo@npm:52.0.49"
dependencies: