@duffel/react-native-components-assistant 0.5.1 → 0.6.0-canary.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.
- package/lib/module/DuffelAssistantForAndroid.js +102 -0
- package/lib/module/DuffelAssistantForAndroid.js.map +1 -0
- package/lib/module/DuffelAssistantForiOS.js +85 -0
- package/lib/module/DuffelAssistantForiOS.js.map +1 -0
- package/lib/module/index.js +23 -113
- package/lib/module/index.js.map +1 -1
- package/lib/module/lib/ShouldStartLoadRequest.js +2 -0
- package/lib/module/lib/ShouldStartLoadRequest.js.map +1 -0
- package/lib/module/lib/WebViewMessageEvent.js +2 -0
- package/lib/module/lib/WebViewMessageEvent.js.map +1 -0
- package/lib/module/lib/assistantIframeUrl.js +4 -0
- package/lib/module/lib/assistantIframeUrl.js.map +1 -0
- package/lib/module/lib/isAssistantIframeUrl.js +12 -0
- package/lib/module/lib/isAssistantIframeUrl.js.map +1 -0
- package/lib/module/lib/isHttpUrl.js +4 -0
- package/lib/module/lib/isHttpUrl.js.map +1 -0
- package/lib/typescript/src/DuffelAssistantForAndroid.d.ts +4 -0
- package/lib/typescript/src/DuffelAssistantForAndroid.d.ts.map +1 -0
- package/lib/typescript/src/DuffelAssistantForiOS.d.ts +4 -0
- package/lib/typescript/src/DuffelAssistantForiOS.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/lib/ShouldStartLoadRequest.d.ts +5 -0
- package/lib/typescript/src/lib/ShouldStartLoadRequest.d.ts.map +1 -0
- package/lib/typescript/src/lib/WebViewMessageEvent.d.ts +6 -0
- package/lib/typescript/src/lib/WebViewMessageEvent.d.ts.map +1 -0
- package/lib/typescript/src/lib/assistantIframeUrl.d.ts +2 -0
- package/lib/typescript/src/lib/assistantIframeUrl.d.ts.map +1 -0
- package/lib/typescript/src/lib/isAssistantIframeUrl.d.ts +2 -0
- package/lib/typescript/src/lib/isAssistantIframeUrl.d.ts.map +1 -0
- package/lib/typescript/src/lib/isHttpUrl.d.ts +2 -0
- package/lib/typescript/src/lib/isHttpUrl.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/DuffelAssistantForAndroid.tsx +131 -0
- package/src/DuffelAssistantForiOS.tsx +101 -0
- package/src/index.tsx +21 -147
- package/src/lib/ShouldStartLoadRequest.ts +4 -0
- package/src/lib/WebViewMessageEvent.ts +5 -0
- package/src/lib/assistantIframeUrl.ts +2 -0
- package/src/lib/isAssistantIframeUrl.ts +11 -0
- package/src/lib/isHttpUrl.ts +1 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { Keyboard, Linking, Modal, StyleSheet, View } from 'react-native';
|
|
5
|
+
import { WebView } from 'react-native-webview';
|
|
6
|
+
import { assistantIframeUrl } from "./lib/assistantIframeUrl.js";
|
|
7
|
+
import { isAssistantIframeUrl } from "./lib/isAssistantIframeUrl.js";
|
|
8
|
+
import { isHttpUrl } from "./lib/isHttpUrl.js";
|
|
9
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
export const DuffelAssistantForAndroid = ({
|
|
11
|
+
isOpen,
|
|
12
|
+
onClose,
|
|
13
|
+
onNewMessage,
|
|
14
|
+
...properties
|
|
15
|
+
}) => {
|
|
16
|
+
const [androidKeyboardHeight, setAndroidKeyboardHeight] = useState(0);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!isOpen) {
|
|
19
|
+
setAndroidKeyboardHeight(0);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const keyboardDidShowSubscription = Keyboard.addListener('keyboardDidShow', event => {
|
|
23
|
+
setAndroidKeyboardHeight(event.endCoordinates?.height ?? 0);
|
|
24
|
+
});
|
|
25
|
+
const keyboardDidHideSubscription = Keyboard.addListener('keyboardDidHide', () => {
|
|
26
|
+
setAndroidKeyboardHeight(0);
|
|
27
|
+
});
|
|
28
|
+
return () => {
|
|
29
|
+
keyboardDidShowSubscription.remove();
|
|
30
|
+
keyboardDidHideSubscription.remove();
|
|
31
|
+
};
|
|
32
|
+
}, [isOpen]);
|
|
33
|
+
const handleMessage = event => {
|
|
34
|
+
if (event.nativeEvent.data === 'duffel-assistant-close') {
|
|
35
|
+
onClose();
|
|
36
|
+
}
|
|
37
|
+
if (typeof properties.onMinimise === 'function' && event.nativeEvent.data === 'duffel-assistant-minimise') {
|
|
38
|
+
properties.onMinimise();
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const parsedData = JSON.parse(event.nativeEvent.data);
|
|
42
|
+
if (parsedData.type === 'duffel-assistant-new-message' && typeof parsedData.userId === 'string' && typeof parsedData.resourceId === 'string' && typeof onNewMessage === 'function') {
|
|
43
|
+
onNewMessage({
|
|
44
|
+
userId: parsedData.userId,
|
|
45
|
+
resourceId: parsedData.resourceId
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// do nothing, invalid JSON
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const handleShouldStartLoadWithRequest = request => {
|
|
53
|
+
if (!isHttpUrl(request.url) || isAssistantIframeUrl(request.url)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
Linking.openURL(request.url).catch(() => {});
|
|
57
|
+
return false;
|
|
58
|
+
};
|
|
59
|
+
return /*#__PURE__*/_jsx(Modal, {
|
|
60
|
+
visible: isOpen,
|
|
61
|
+
onRequestClose: () => onClose(),
|
|
62
|
+
animationType: "slide",
|
|
63
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
64
|
+
style: [styles.androidContainer, {
|
|
65
|
+
paddingBottom: 15 + androidKeyboardHeight
|
|
66
|
+
}],
|
|
67
|
+
children: /*#__PURE__*/_jsx(WebView
|
|
68
|
+
// Required for Android
|
|
69
|
+
, {
|
|
70
|
+
injectedJavaScript: `
|
|
71
|
+
postMessage(
|
|
72
|
+
{
|
|
73
|
+
type: "duffel-assistant-open",
|
|
74
|
+
properties: ${JSON.stringify(properties)},
|
|
75
|
+
},
|
|
76
|
+
"*",
|
|
77
|
+
);
|
|
78
|
+
true;`,
|
|
79
|
+
source: {
|
|
80
|
+
uri: assistantIframeUrl
|
|
81
|
+
},
|
|
82
|
+
onMessage: handleMessage,
|
|
83
|
+
onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest
|
|
84
|
+
// Always fetch the latest assistant bundle. The iframe.html shell
|
|
85
|
+
// references `./iframe-app.js` by a stable path, so without this
|
|
86
|
+
// the system WebView can serve a stale bundle for a long time.
|
|
87
|
+
,
|
|
88
|
+
cacheEnabled: false,
|
|
89
|
+
cacheMode: "LOAD_NO_CACHE"
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const styles = StyleSheet.create({
|
|
95
|
+
androidContainer: {
|
|
96
|
+
flex: 1,
|
|
97
|
+
width: '100%',
|
|
98
|
+
paddingTop: 45,
|
|
99
|
+
paddingBottom: 15
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
//# sourceMappingURL=DuffelAssistantForAndroid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["React","useEffect","useState","Keyboard","Linking","Modal","StyleSheet","View","WebView","assistantIframeUrl","isAssistantIframeUrl","isHttpUrl","jsx","_jsx","DuffelAssistantForAndroid","isOpen","onClose","onNewMessage","properties","androidKeyboardHeight","setAndroidKeyboardHeight","keyboardDidShowSubscription","addListener","event","endCoordinates","height","keyboardDidHideSubscription","remove","handleMessage","nativeEvent","data","onMinimise","parsedData","JSON","parse","type","userId","resourceId","error","handleShouldStartLoadWithRequest","request","url","openURL","catch","visible","onRequestClose","animationType","children","style","styles","androidContainer","paddingBottom","injectedJavaScript","stringify","source","uri","onMessage","onShouldStartLoadWithRequest","cacheEnabled","cacheMode","create","flex","width","paddingTop"],"sourceRoot":"../../src","sources":["DuffelAssistantForAndroid.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { KeyboardAvoidingView, Linking, Modal, StyleSheet } from 'react-native';
|
|
5
|
+
import { WebView } from 'react-native-webview';
|
|
6
|
+
import { assistantIframeUrl } from "./lib/assistantIframeUrl.js";
|
|
7
|
+
import { isAssistantIframeUrl } from "./lib/isAssistantIframeUrl.js";
|
|
8
|
+
import { isHttpUrl } from "./lib/isHttpUrl.js";
|
|
9
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
export const DuffelAssistantForiOS = ({
|
|
11
|
+
isOpen,
|
|
12
|
+
onClose,
|
|
13
|
+
onNewMessage,
|
|
14
|
+
...properties
|
|
15
|
+
}) => {
|
|
16
|
+
const handleMessage = event => {
|
|
17
|
+
if (event.nativeEvent.data === 'duffel-assistant-close') {
|
|
18
|
+
onClose();
|
|
19
|
+
}
|
|
20
|
+
if (typeof properties.onMinimise === 'function' && event.nativeEvent.data === 'duffel-assistant-minimise') {
|
|
21
|
+
properties.onMinimise();
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const parsedData = JSON.parse(event.nativeEvent.data);
|
|
25
|
+
if (parsedData.type === 'duffel-assistant-new-message' && typeof parsedData.userId === 'string' && typeof parsedData.resourceId === 'string' && typeof onNewMessage === 'function') {
|
|
26
|
+
onNewMessage({
|
|
27
|
+
userId: parsedData.userId,
|
|
28
|
+
resourceId: parsedData.resourceId
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
// do nothing, invalid JSON
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const handleShouldStartLoadWithRequest = request => {
|
|
36
|
+
if (!isHttpUrl(request.url) || isAssistantIframeUrl(request.url)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (request.navigationType !== 'click') {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
Linking.openURL(request.url).catch(() => {});
|
|
43
|
+
return false;
|
|
44
|
+
};
|
|
45
|
+
return /*#__PURE__*/_jsx(Modal, {
|
|
46
|
+
visible: isOpen,
|
|
47
|
+
onRequestClose: () => onClose(),
|
|
48
|
+
animationType: "slide",
|
|
49
|
+
children: /*#__PURE__*/_jsx(KeyboardAvoidingView, {
|
|
50
|
+
behavior: "padding",
|
|
51
|
+
style: styles.container,
|
|
52
|
+
children: /*#__PURE__*/_jsx(WebView, {
|
|
53
|
+
style: styles.webView,
|
|
54
|
+
injectedJavaScriptObject: properties
|
|
55
|
+
// When you are running this in development iOS doesn't allow request from hosts with invalid https certificates.
|
|
56
|
+
// If you try ngrok with the local esbuild server it will fail because ngrok requests in https and the dev server rejects http requests.
|
|
57
|
+
// Easiest solution is to use the production URL with test data.
|
|
58
|
+
// If that's not possible, you can also upload the assistant built assets to a different folder in assets.duffel and work with that.
|
|
59
|
+
,
|
|
60
|
+
source: {
|
|
61
|
+
uri: assistantIframeUrl
|
|
62
|
+
},
|
|
63
|
+
onMessage: handleMessage,
|
|
64
|
+
onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest
|
|
65
|
+
// Always fetch the latest assistant bundle. The iframe.html shell
|
|
66
|
+
// references `./iframe-app.js` by a stable path, so without this
|
|
67
|
+
// the system WebView can serve a stale bundle for a long time.
|
|
68
|
+
,
|
|
69
|
+
cacheEnabled: false
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const styles = StyleSheet.create({
|
|
75
|
+
container: {
|
|
76
|
+
height: '100%',
|
|
77
|
+
width: '100%',
|
|
78
|
+
paddingHorizontal: 12,
|
|
79
|
+
paddingTop: 50
|
|
80
|
+
},
|
|
81
|
+
webView: {
|
|
82
|
+
paddingBottom: 7
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
//# sourceMappingURL=DuffelAssistantForiOS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["React","KeyboardAvoidingView","Linking","Modal","StyleSheet","WebView","assistantIframeUrl","isAssistantIframeUrl","isHttpUrl","jsx","_jsx","DuffelAssistantForiOS","isOpen","onClose","onNewMessage","properties","handleMessage","event","nativeEvent","data","onMinimise","parsedData","JSON","parse","type","userId","resourceId","error","handleShouldStartLoadWithRequest","request","url","navigationType","openURL","catch","visible","onRequestClose","animationType","children","behavior","style","styles","container","webView","injectedJavaScriptObject","source","uri","onMessage","onShouldStartLoadWithRequest","cacheEnabled","create","height","width","paddingHorizontal","paddingTop","paddingBottom"],"sourceRoot":"../../src","sources":["DuffelAssistantForiOS.tsx"],"mappings":"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","ignoreList":[]}
|
package/lib/module/index.js
CHANGED
|
@@ -1,31 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { Platform } from 'react-native';
|
|
5
|
+
import { DuffelAssistantForAndroid } from "./DuffelAssistantForAndroid.js";
|
|
6
|
+
import { DuffelAssistantForiOS } from "./DuffelAssistantForiOS.js";
|
|
6
7
|
import { hasJsonWebTokenFormat } from "./lib/hasJsonWebTokenFormat.js";
|
|
7
8
|
import { getClientKeyPayload } from "./lib/getClientKeyPayload.js";
|
|
8
9
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const isAssistantIframeUrl = url => {
|
|
12
|
-
try {
|
|
13
|
-
const parsedUrl = new URL(url);
|
|
14
|
-
return `${parsedUrl.origin}${parsedUrl.pathname}` === assistantIframeUrl;
|
|
15
|
-
} catch (error) {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
export const DuffelAssistant = ({
|
|
20
|
-
isOpen,
|
|
21
|
-
onClose,
|
|
22
|
-
onNewMessage,
|
|
23
|
-
...properties
|
|
24
|
-
}) => {
|
|
25
|
-
if (!hasJsonWebTokenFormat(properties.clientKey)) {
|
|
10
|
+
export const DuffelAssistant = props => {
|
|
11
|
+
if (!hasJsonWebTokenFormat(props.clientKey)) {
|
|
26
12
|
throw new Error('Duffel Assistant client key format must be a valid JWT.');
|
|
27
13
|
}
|
|
28
|
-
const clientKeyPayload = getClientKeyPayload(
|
|
14
|
+
const clientKeyPayload = getClientKeyPayload(props.clientKey);
|
|
29
15
|
if (!clientKeyPayload) {
|
|
30
16
|
throw new Error('The client key property is invalid. A client key must follow the JWT format.');
|
|
31
17
|
}
|
|
@@ -33,108 +19,32 @@ export const DuffelAssistant = ({
|
|
|
33
19
|
if (!('user_id' in clientKeyPayload)) {
|
|
34
20
|
throw new Error('The client key in the props does not include a user_id. Make sure a user_id is included in the payload of the client key creation.');
|
|
35
21
|
}
|
|
36
|
-
if (typeof
|
|
22
|
+
if (typeof props.issueType !== 'undefined') {
|
|
37
23
|
console.warn('The issueType prop has been deprecated and will no longer be supported in the future. Please use the context prop instead.');
|
|
38
24
|
}
|
|
39
|
-
|
|
25
|
+
const sanitizedProps = {
|
|
26
|
+
...props
|
|
27
|
+
};
|
|
28
|
+
if (!clientKeyIncludesResource && typeof sanitizedProps.issueType !== 'undefined') {
|
|
40
29
|
console.warn('The issueType prop is only supported when the client key includes a resource id, it will be ignored.');
|
|
41
|
-
delete
|
|
30
|
+
delete sanitizedProps.issueType;
|
|
42
31
|
}
|
|
43
|
-
if (!clientKeyIncludesResource &&
|
|
32
|
+
if (!clientKeyIncludesResource && sanitizedProps.context) {
|
|
44
33
|
throw new Error('The context prop is only supported when the client key includes a resource id.');
|
|
45
34
|
}
|
|
46
|
-
if (typeof
|
|
47
|
-
throw new Error(
|
|
35
|
+
if (typeof sanitizedProps.supportChannels !== 'undefined' && sanitizedProps.supportChannels.chat !== true) {
|
|
36
|
+
throw new Error("The supportChannels prop must keep chat enabled. Phone-only support isn't offered.");
|
|
48
37
|
}
|
|
49
|
-
if (
|
|
38
|
+
if (sanitizedProps.showMinimiseButton && typeof sanitizedProps.onMinimise !== 'function') {
|
|
50
39
|
console.warn('The showMinimiseButton prop is set to true, but the onMinimise callback is not provided. Make sure to listen to the onMinimise callback to handle the minimisation of the Assistant.');
|
|
51
40
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const parsedData = JSON.parse(event.nativeEvent.data);
|
|
61
|
-
if (parsedData.type === 'duffel-assistant-new-message' && typeof parsedData.userId === 'string' && typeof parsedData.resourceId === 'string' && typeof onNewMessage === 'function') {
|
|
62
|
-
onNewMessage({
|
|
63
|
-
userId: parsedData.userId,
|
|
64
|
-
resourceId: parsedData.resourceId
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
} catch (error) {
|
|
68
|
-
// do nothing, invalid JSON
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
const handleShouldStartLoadWithRequest = request => {
|
|
72
|
-
if (!isHttpUrl(request.url) || isAssistantIframeUrl(request.url)) {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
if (Platform.OS === 'ios' && request.navigationType !== 'click') {
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
Linking.openURL(request.url).catch(() => {});
|
|
79
|
-
return false;
|
|
80
|
-
};
|
|
81
|
-
return /*#__PURE__*/_jsx(Modal, {
|
|
82
|
-
visible: isOpen,
|
|
83
|
-
onRequestClose: () => onClose(),
|
|
84
|
-
animationType: "slide",
|
|
85
|
-
children: /*#__PURE__*/_jsx(View, {
|
|
86
|
-
style: styles.container,
|
|
87
|
-
children: Platform.OS === 'ios' ? /*#__PURE__*/_jsx(WebView, {
|
|
88
|
-
injectedJavaScriptObject: properties
|
|
89
|
-
// When you are running this in development iOS doesn't allow request from hosts with invalid https certificates.
|
|
90
|
-
// If you try ngrok with the local esbuild server it will fail because ngrok requests in https and the dev server rejects http requests.
|
|
91
|
-
// Easiest solution is to use the production URL with test data.
|
|
92
|
-
// If that's not possible, you can also upload the assistant built assets to a different folder in assets.duffel and work with that.
|
|
93
|
-
,
|
|
94
|
-
source: {
|
|
95
|
-
uri: assistantIframeUrl
|
|
96
|
-
},
|
|
97
|
-
onMessage: handleMessage,
|
|
98
|
-
onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest
|
|
99
|
-
// Always fetch the latest assistant bundle. The iframe.html shell
|
|
100
|
-
// references `./iframe-app.js` by a stable path, so without this
|
|
101
|
-
// the system WebView can serve a stale bundle for a long time.
|
|
102
|
-
,
|
|
103
|
-
cacheEnabled: false
|
|
104
|
-
}) : /*#__PURE__*/_jsx(WebView
|
|
105
|
-
// Required for Android
|
|
106
|
-
, {
|
|
107
|
-
injectedJavaScript: `
|
|
108
|
-
postMessage(
|
|
109
|
-
{
|
|
110
|
-
type: "duffel-assistant-open",
|
|
111
|
-
properties: ${JSON.stringify(properties)},
|
|
112
|
-
},
|
|
113
|
-
"*",
|
|
114
|
-
);
|
|
115
|
-
true;`,
|
|
116
|
-
source: {
|
|
117
|
-
uri: assistantIframeUrl
|
|
118
|
-
},
|
|
119
|
-
onMessage: handleMessage,
|
|
120
|
-
onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest
|
|
121
|
-
// Always fetch the latest assistant bundle. The iframe.html shell
|
|
122
|
-
// references `./iframe-app.js` by a stable path, so without this
|
|
123
|
-
// the system WebView can serve a stale bundle for a long time.
|
|
124
|
-
,
|
|
125
|
-
cacheEnabled: false,
|
|
126
|
-
cacheMode: "LOAD_NO_CACHE"
|
|
127
|
-
})
|
|
128
|
-
})
|
|
41
|
+
if (Platform.OS === 'android') {
|
|
42
|
+
return /*#__PURE__*/_jsx(DuffelAssistantForAndroid, {
|
|
43
|
+
...sanitizedProps
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return /*#__PURE__*/_jsx(DuffelAssistantForiOS, {
|
|
47
|
+
...sanitizedProps
|
|
129
48
|
});
|
|
130
49
|
};
|
|
131
|
-
const styles = StyleSheet.create({
|
|
132
|
-
container: {
|
|
133
|
-
height: '100%',
|
|
134
|
-
width: '100%',
|
|
135
|
-
paddingHorizontal: 12,
|
|
136
|
-
paddingTop: 50,
|
|
137
|
-
paddingBottom: 30
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
50
|
//# sourceMappingURL=index.js.map
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["React","
|
|
1
|
+
{"version":3,"names":["React","Platform","DuffelAssistantForAndroid","DuffelAssistantForiOS","hasJsonWebTokenFormat","getClientKeyPayload","jsx","_jsx","DuffelAssistant","props","clientKey","Error","clientKeyPayload","clientKeyIncludesResource","order_id","undefined","booking_id","cars_booking_id","issueType","console","warn","sanitizedProps","context","supportChannels","chat","showMinimiseButton","onMinimise","OS"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SAASC,QAAQ,QAAQ,cAAc;AAEvC,SAASC,yBAAyB,QAAQ,gCAA6B;AACvE,SAASC,qBAAqB,QAAQ,4BAAyB;AAC/D,SAASC,qBAAqB,QAAQ,gCAA6B;AACnE,SAASC,mBAAmB,QAAQ,8BAA2B;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAIhE,OAAO,MAAMC,eAA+C,GAAIC,KAAK,IAAK;EACxE,IAAI,CAACL,qBAAqB,CAACK,KAAK,CAACC,SAAS,CAAC,EAAE;IAC3C,MAAM,IAAIC,KAAK,CAAC,yDAAyD,CAAC;EAC5E;EAEA,MAAMC,gBAAgB,GAAGP,mBAAmB,CAACI,KAAK,CAACC,SAAS,CAAC;EAC7D,IAAI,CAACE,gBAAgB,EAAE;IACrB,MAAM,IAAID,KAAK,CACb,8EACF,CAAC;EACH;EAEA,MAAME,yBAAyB,GAC7BD,gBAAgB,CAACE,QAAQ,KAAKC,SAAS,IACvCH,gBAAgB,CAACI,UAAU,KAAKD,SAAS,IACzCH,gBAAgB,CAACK,eAAe,KAAKF,SAAS;EAEhD,IAAI,EAAE,SAAS,IAAIH,gBAAgB,CAAC,EAAE;IACpC,MAAM,IAAID,KAAK,CACb,oIACF,CAAC;EACH;EAEA,IAAI,OAAOF,KAAK,CAACS,SAAS,KAAK,WAAW,EAAE;IAC1CC,OAAO,CAACC,IAAI,CACV,4HACF,CAAC;EACH;EAEA,MAAMC,cAAoC,GAAG;IAAE,GAAGZ;EAAM,CAAC;EAEzD,IACE,CAACI,yBAAyB,IAC1B,OAAOQ,cAAc,CAACH,SAAS,KAAK,WAAW,EAC/C;IACAC,OAAO,CAACC,IAAI,CACV,sGACF,CAAC;IACD,OAAOC,cAAc,CAACH,SAAS;EACjC;EAEA,IAAI,CAACL,yBAAyB,IAAIQ,cAAc,CAACC,OAAO,EAAE;IACxD,MAAM,IAAIX,KAAK,CACb,gFACF,CAAC;EACH;EAEA,IACE,OAAOU,cAAc,CAACE,eAAe,KAAK,WAAW,IACrDF,cAAc,CAACE,eAAe,CAACC,IAAI,KAAK,IAAI,EAC5C;IACA,MAAM,IAAIb,KAAK,CACb,oFACF,CAAC;EACH;EAEA,IACEU,cAAc,CAACI,kBAAkB,IACjC,OAAOJ,cAAc,CAACK,UAAU,KAAK,UAAU,EAC/C;IACAP,OAAO,CAACC,IAAI,CACV,sLACF,CAAC;EACH;EAEA,IAAInB,QAAQ,CAAC0B,EAAE,KAAK,SAAS,EAAE;IAC7B,oBAAOpB,IAAA,CAACL,yBAAyB;MAAA,GAAKmB;IAAc,CAAG,CAAC;EAC1D;EAEA,oBAAOd,IAAA,CAACJ,qBAAqB;IAAA,GAAKkB;EAAc,CAAG,CAAC;AACtD,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["lib/ShouldStartLoadRequest.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["lib/WebViewMessageEvent.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["assistantIframeUrl"],"sourceRoot":"../../../src","sources":["lib/assistantIframeUrl.ts"],"mappings":";;AAAA,OAAO,MAAMA,kBAAkB,GAC7B,iDAAiD","ignoreList":[]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { assistantIframeUrl } from "./assistantIframeUrl.js";
|
|
4
|
+
export const isAssistantIframeUrl = url => {
|
|
5
|
+
try {
|
|
6
|
+
const parsedUrl = new URL(url);
|
|
7
|
+
return `${parsedUrl.origin}${parsedUrl.pathname}` === assistantIframeUrl;
|
|
8
|
+
} catch (error) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=isAssistantIframeUrl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["assistantIframeUrl","isAssistantIframeUrl","url","parsedUrl","URL","origin","pathname","error"],"sourceRoot":"../../../src","sources":["lib/isAssistantIframeUrl.ts"],"mappings":";;AAAA,SAASA,kBAAkB,QAAQ,yBAAsB;AAEzD,OAAO,MAAMC,oBAAoB,GAAIC,GAAW,IAAK;EACnD,IAAI;IACF,MAAMC,SAAS,GAAG,IAAIC,GAAG,CAACF,GAAG,CAAC;IAE9B,OAAO,GAAGC,SAAS,CAACE,MAAM,GAAGF,SAAS,CAACG,QAAQ,EAAE,KAAKN,kBAAkB;EAC1E,CAAC,CAAC,OAAOO,KAAK,EAAE;IACd,OAAO,KAAK;EACd;AACF,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["isHttpUrl","url","test"],"sourceRoot":"../../../src","sources":["lib/isHttpUrl.ts"],"mappings":";;AAAA,OAAO,MAAMA,SAAS,GAAIC,GAAW,IAAK,cAAc,CAACC,IAAI,CAACD,GAAG,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DuffelAssistantForAndroid.d.ts","sourceRoot":"","sources":["../../../src/DuffelAssistantForAndroid.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAGnD,OAAO,EAAE,KAAK,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAOnE,eAAO,MAAM,yBAAyB,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA+GpE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DuffelAssistantForiOS.d.ts","sourceRoot":"","sources":["../../../src/DuffelAssistantForiOS.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,KAAK,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAOnE,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA8EhE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,KAAK,oBAAoB,IAAI,4BAA4B,EAAE,MAAM,wBAAwB,CAAC;AAMnG,MAAM,MAAM,oBAAoB,GAAG,4BAA4B,CAAC;AAEhE,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAsE1D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShouldStartLoadRequest.d.ts","sourceRoot":"","sources":["../../../../src/lib/ShouldStartLoadRequest.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebViewMessageEvent.d.ts","sourceRoot":"","sources":["../../../../src/lib/WebViewMessageEvent.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistantIframeUrl.d.ts","sourceRoot":"","sources":["../../../../src/lib/assistantIframeUrl.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,oDACoB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isAssistantIframeUrl.d.ts","sourceRoot":"","sources":["../../../../src/lib/isAssistantIframeUrl.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,oBAAoB,GAAI,KAAK,MAAM,YAQ/C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isHttpUrl.d.ts","sourceRoot":"","sources":["../../../../src/lib/isHttpUrl.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS,GAAI,KAAK,MAAM,YAA6B,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Keyboard, Linking, Modal, StyleSheet, View } from 'react-native';
|
|
3
|
+
import { WebView } from 'react-native-webview';
|
|
4
|
+
import { type DuffelAssistantProps } from './DuffelAssistantProps';
|
|
5
|
+
import { assistantIframeUrl } from './lib/assistantIframeUrl';
|
|
6
|
+
import { isAssistantIframeUrl } from './lib/isAssistantIframeUrl';
|
|
7
|
+
import { isHttpUrl } from './lib/isHttpUrl';
|
|
8
|
+
import { type ShouldStartLoadRequest } from './lib/ShouldStartLoadRequest';
|
|
9
|
+
import { type WebViewMessageEvent } from './lib/WebViewMessageEvent';
|
|
10
|
+
|
|
11
|
+
export const DuffelAssistantForAndroid: React.FC<DuffelAssistantProps> = ({
|
|
12
|
+
isOpen,
|
|
13
|
+
onClose,
|
|
14
|
+
onNewMessage,
|
|
15
|
+
...properties
|
|
16
|
+
}) => {
|
|
17
|
+
const [androidKeyboardHeight, setAndroidKeyboardHeight] = useState(0);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!isOpen) {
|
|
21
|
+
setAndroidKeyboardHeight(0);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const keyboardDidShowSubscription = Keyboard.addListener(
|
|
26
|
+
'keyboardDidShow',
|
|
27
|
+
(event) => {
|
|
28
|
+
setAndroidKeyboardHeight(event.endCoordinates?.height ?? 0);
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
const keyboardDidHideSubscription = Keyboard.addListener(
|
|
32
|
+
'keyboardDidHide',
|
|
33
|
+
() => {
|
|
34
|
+
setAndroidKeyboardHeight(0);
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
keyboardDidShowSubscription.remove();
|
|
40
|
+
keyboardDidHideSubscription.remove();
|
|
41
|
+
};
|
|
42
|
+
}, [isOpen]);
|
|
43
|
+
|
|
44
|
+
const handleMessage = (event: WebViewMessageEvent) => {
|
|
45
|
+
if (event.nativeEvent.data === 'duffel-assistant-close') {
|
|
46
|
+
onClose();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
typeof properties.onMinimise === 'function' &&
|
|
51
|
+
event.nativeEvent.data === 'duffel-assistant-minimise'
|
|
52
|
+
) {
|
|
53
|
+
properties.onMinimise();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const parsedData = JSON.parse(event.nativeEvent.data);
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
parsedData.type === 'duffel-assistant-new-message' &&
|
|
61
|
+
typeof parsedData.userId === 'string' &&
|
|
62
|
+
typeof parsedData.resourceId === 'string' &&
|
|
63
|
+
typeof onNewMessage === 'function'
|
|
64
|
+
) {
|
|
65
|
+
onNewMessage({
|
|
66
|
+
userId: parsedData.userId,
|
|
67
|
+
resourceId: parsedData.resourceId,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// do nothing, invalid JSON
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleShouldStartLoadWithRequest = (
|
|
76
|
+
request: ShouldStartLoadRequest
|
|
77
|
+
) => {
|
|
78
|
+
if (!isHttpUrl(request.url) || isAssistantIframeUrl(request.url)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Linking.openURL(request.url).catch(() => {});
|
|
83
|
+
|
|
84
|
+
return false;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Modal
|
|
89
|
+
visible={isOpen}
|
|
90
|
+
onRequestClose={() => onClose()}
|
|
91
|
+
animationType="slide"
|
|
92
|
+
>
|
|
93
|
+
<View
|
|
94
|
+
style={[
|
|
95
|
+
styles.androidContainer,
|
|
96
|
+
{ paddingBottom: 15 + androidKeyboardHeight },
|
|
97
|
+
]}
|
|
98
|
+
>
|
|
99
|
+
<WebView
|
|
100
|
+
// Required for Android
|
|
101
|
+
injectedJavaScript={`
|
|
102
|
+
postMessage(
|
|
103
|
+
{
|
|
104
|
+
type: "duffel-assistant-open",
|
|
105
|
+
properties: ${JSON.stringify(properties)},
|
|
106
|
+
},
|
|
107
|
+
"*",
|
|
108
|
+
);
|
|
109
|
+
true;`}
|
|
110
|
+
source={{ uri: assistantIframeUrl }}
|
|
111
|
+
onMessage={handleMessage}
|
|
112
|
+
onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
|
|
113
|
+
// Always fetch the latest assistant bundle. The iframe.html shell
|
|
114
|
+
// references `./iframe-app.js` by a stable path, so without this
|
|
115
|
+
// the system WebView can serve a stale bundle for a long time.
|
|
116
|
+
cacheEnabled={false}
|
|
117
|
+
cacheMode="LOAD_NO_CACHE"
|
|
118
|
+
/>
|
|
119
|
+
</View>
|
|
120
|
+
</Modal>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const styles = StyleSheet.create({
|
|
125
|
+
androidContainer: {
|
|
126
|
+
flex: 1,
|
|
127
|
+
width: '100%',
|
|
128
|
+
paddingTop: 45,
|
|
129
|
+
paddingBottom: 15,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { KeyboardAvoidingView, Linking, Modal, StyleSheet } from 'react-native';
|
|
3
|
+
import { WebView } from 'react-native-webview';
|
|
4
|
+
import { type DuffelAssistantProps } from './DuffelAssistantProps';
|
|
5
|
+
import { assistantIframeUrl } from './lib/assistantIframeUrl';
|
|
6
|
+
import { isAssistantIframeUrl } from './lib/isAssistantIframeUrl';
|
|
7
|
+
import { isHttpUrl } from './lib/isHttpUrl';
|
|
8
|
+
import { type ShouldStartLoadRequest } from './lib/ShouldStartLoadRequest';
|
|
9
|
+
import { type WebViewMessageEvent } from './lib/WebViewMessageEvent';
|
|
10
|
+
|
|
11
|
+
export const DuffelAssistantForiOS: React.FC<DuffelAssistantProps> = ({
|
|
12
|
+
isOpen,
|
|
13
|
+
onClose,
|
|
14
|
+
onNewMessage,
|
|
15
|
+
...properties
|
|
16
|
+
}) => {
|
|
17
|
+
const handleMessage = (event: WebViewMessageEvent) => {
|
|
18
|
+
if (event.nativeEvent.data === 'duffel-assistant-close') {
|
|
19
|
+
onClose();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
typeof properties.onMinimise === 'function' &&
|
|
24
|
+
event.nativeEvent.data === 'duffel-assistant-minimise'
|
|
25
|
+
) {
|
|
26
|
+
properties.onMinimise();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const parsedData = JSON.parse(event.nativeEvent.data);
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
parsedData.type === 'duffel-assistant-new-message' &&
|
|
34
|
+
typeof parsedData.userId === 'string' &&
|
|
35
|
+
typeof parsedData.resourceId === 'string' &&
|
|
36
|
+
typeof onNewMessage === 'function'
|
|
37
|
+
) {
|
|
38
|
+
onNewMessage({
|
|
39
|
+
userId: parsedData.userId,
|
|
40
|
+
resourceId: parsedData.resourceId,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// do nothing, invalid JSON
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleShouldStartLoadWithRequest = (
|
|
49
|
+
request: ShouldStartLoadRequest
|
|
50
|
+
) => {
|
|
51
|
+
if (!isHttpUrl(request.url) || isAssistantIframeUrl(request.url)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (request.navigationType !== 'click') {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Linking.openURL(request.url).catch(() => {});
|
|
60
|
+
|
|
61
|
+
return false;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Modal
|
|
66
|
+
visible={isOpen}
|
|
67
|
+
onRequestClose={() => onClose()}
|
|
68
|
+
animationType="slide"
|
|
69
|
+
>
|
|
70
|
+
<KeyboardAvoidingView behavior="padding" style={styles.container}>
|
|
71
|
+
<WebView
|
|
72
|
+
style={styles.webView}
|
|
73
|
+
injectedJavaScriptObject={properties}
|
|
74
|
+
// When you are running this in development iOS doesn't allow request from hosts with invalid https certificates.
|
|
75
|
+
// If you try ngrok with the local esbuild server it will fail because ngrok requests in https and the dev server rejects http requests.
|
|
76
|
+
// Easiest solution is to use the production URL with test data.
|
|
77
|
+
// If that's not possible, you can also upload the assistant built assets to a different folder in assets.duffel and work with that.
|
|
78
|
+
source={{ uri: assistantIframeUrl }}
|
|
79
|
+
onMessage={handleMessage}
|
|
80
|
+
onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
|
|
81
|
+
// Always fetch the latest assistant bundle. The iframe.html shell
|
|
82
|
+
// references `./iframe-app.js` by a stable path, so without this
|
|
83
|
+
// the system WebView can serve a stale bundle for a long time.
|
|
84
|
+
cacheEnabled={false}
|
|
85
|
+
/>
|
|
86
|
+
</KeyboardAvoidingView>
|
|
87
|
+
</Modal>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const styles = StyleSheet.create({
|
|
92
|
+
container: {
|
|
93
|
+
height: '100%',
|
|
94
|
+
width: '100%',
|
|
95
|
+
paddingHorizontal: 12,
|
|
96
|
+
paddingTop: 50,
|
|
97
|
+
},
|
|
98
|
+
webView: {
|
|
99
|
+
paddingBottom: 7,
|
|
100
|
+
},
|
|
101
|
+
});
|
package/src/index.tsx
CHANGED
|
@@ -1,48 +1,19 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import { WebView } from 'react-native-webview';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
4
3
|
import { type DuffelAssistantProps as DuffelAssistantPropsImported } from './DuffelAssistantProps';
|
|
4
|
+
import { DuffelAssistantForAndroid } from './DuffelAssistantForAndroid';
|
|
5
|
+
import { DuffelAssistantForiOS } from './DuffelAssistantForiOS';
|
|
5
6
|
import { hasJsonWebTokenFormat } from './lib/hasJsonWebTokenFormat';
|
|
6
7
|
import { getClientKeyPayload } from './lib/getClientKeyPayload';
|
|
7
8
|
|
|
8
9
|
export type DuffelAssistantProps = DuffelAssistantPropsImported;
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
type WebViewMessageEvent = {
|
|
13
|
-
nativeEvent: {
|
|
14
|
-
data: string;
|
|
15
|
-
};
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
type ShouldStartLoadRequest = {
|
|
19
|
-
navigationType?: string;
|
|
20
|
-
url: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const isHttpUrl = (url: string) => /^https?:\/\//.test(url);
|
|
24
|
-
|
|
25
|
-
const isAssistantIframeUrl = (url: string) => {
|
|
26
|
-
try {
|
|
27
|
-
const parsedUrl = new URL(url);
|
|
28
|
-
|
|
29
|
-
return `${parsedUrl.origin}${parsedUrl.pathname}` === assistantIframeUrl;
|
|
30
|
-
} catch (error) {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export const DuffelAssistant: React.FC<DuffelAssistantProps> = ({
|
|
36
|
-
isOpen,
|
|
37
|
-
onClose,
|
|
38
|
-
onNewMessage,
|
|
39
|
-
...properties
|
|
40
|
-
}) => {
|
|
41
|
-
if (!hasJsonWebTokenFormat(properties.clientKey)) {
|
|
11
|
+
export const DuffelAssistant: React.FC<DuffelAssistantProps> = (props) => {
|
|
12
|
+
if (!hasJsonWebTokenFormat(props.clientKey)) {
|
|
42
13
|
throw new Error('Duffel Assistant client key format must be a valid JWT.');
|
|
43
14
|
}
|
|
44
15
|
|
|
45
|
-
const clientKeyPayload = getClientKeyPayload(
|
|
16
|
+
const clientKeyPayload = getClientKeyPayload(props.clientKey);
|
|
46
17
|
if (!clientKeyPayload) {
|
|
47
18
|
throw new Error(
|
|
48
19
|
'The client key property is invalid. A client key must follow the JWT format.'
|
|
@@ -60,148 +31,51 @@ export const DuffelAssistant: React.FC<DuffelAssistantProps> = ({
|
|
|
60
31
|
);
|
|
61
32
|
}
|
|
62
33
|
|
|
63
|
-
if (typeof
|
|
34
|
+
if (typeof props.issueType !== 'undefined') {
|
|
64
35
|
console.warn(
|
|
65
36
|
'The issueType prop has been deprecated and will no longer be supported in the future. Please use the context prop instead.'
|
|
66
37
|
);
|
|
67
38
|
}
|
|
68
39
|
|
|
40
|
+
const sanitizedProps: DuffelAssistantProps = { ...props };
|
|
41
|
+
|
|
69
42
|
if (
|
|
70
43
|
!clientKeyIncludesResource &&
|
|
71
|
-
typeof
|
|
44
|
+
typeof sanitizedProps.issueType !== 'undefined'
|
|
72
45
|
) {
|
|
73
46
|
console.warn(
|
|
74
47
|
'The issueType prop is only supported when the client key includes a resource id, it will be ignored.'
|
|
75
48
|
);
|
|
76
|
-
delete
|
|
49
|
+
delete sanitizedProps.issueType;
|
|
77
50
|
}
|
|
78
51
|
|
|
79
|
-
if (!clientKeyIncludesResource &&
|
|
52
|
+
if (!clientKeyIncludesResource && sanitizedProps.context) {
|
|
80
53
|
throw new Error(
|
|
81
54
|
'The context prop is only supported when the client key includes a resource id.'
|
|
82
55
|
);
|
|
83
56
|
}
|
|
84
57
|
|
|
85
58
|
if (
|
|
86
|
-
typeof
|
|
87
|
-
|
|
59
|
+
typeof sanitizedProps.supportChannels !== 'undefined' &&
|
|
60
|
+
sanitizedProps.supportChannels.chat !== true
|
|
88
61
|
) {
|
|
89
62
|
throw new Error(
|
|
90
|
-
|
|
63
|
+
"The supportChannels prop must keep chat enabled. Phone-only support isn't offered."
|
|
91
64
|
);
|
|
92
65
|
}
|
|
93
66
|
|
|
94
67
|
if (
|
|
95
|
-
|
|
96
|
-
typeof
|
|
68
|
+
sanitizedProps.showMinimiseButton &&
|
|
69
|
+
typeof sanitizedProps.onMinimise !== 'function'
|
|
97
70
|
) {
|
|
98
71
|
console.warn(
|
|
99
72
|
'The showMinimiseButton prop is set to true, but the onMinimise callback is not provided. Make sure to listen to the onMinimise callback to handle the minimisation of the Assistant.'
|
|
100
73
|
);
|
|
101
74
|
}
|
|
102
75
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
typeof properties.onMinimise === 'function' &&
|
|
110
|
-
event.nativeEvent.data === 'duffel-assistant-minimise'
|
|
111
|
-
) {
|
|
112
|
-
properties.onMinimise();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
const parsedData = JSON.parse(event.nativeEvent.data);
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
parsedData.type === 'duffel-assistant-new-message' &&
|
|
120
|
-
typeof parsedData.userId === 'string' &&
|
|
121
|
-
typeof parsedData.resourceId === 'string' &&
|
|
122
|
-
typeof onNewMessage === 'function'
|
|
123
|
-
) {
|
|
124
|
-
onNewMessage({
|
|
125
|
-
userId: parsedData.userId,
|
|
126
|
-
resourceId: parsedData.resourceId,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
} catch (error) {
|
|
130
|
-
// do nothing, invalid JSON
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const handleShouldStartLoadWithRequest = (
|
|
135
|
-
request: ShouldStartLoadRequest
|
|
136
|
-
) => {
|
|
137
|
-
if (!isHttpUrl(request.url) || isAssistantIframeUrl(request.url)) {
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (Platform.OS === 'ios' && request.navigationType !== 'click') {
|
|
142
|
-
return true;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
Linking.openURL(request.url).catch(() => {});
|
|
146
|
-
|
|
147
|
-
return false;
|
|
148
|
-
};
|
|
76
|
+
if (Platform.OS === 'android') {
|
|
77
|
+
return <DuffelAssistantForAndroid {...sanitizedProps} />;
|
|
78
|
+
}
|
|
149
79
|
|
|
150
|
-
return
|
|
151
|
-
<Modal
|
|
152
|
-
visible={isOpen}
|
|
153
|
-
onRequestClose={() => onClose()}
|
|
154
|
-
animationType="slide"
|
|
155
|
-
>
|
|
156
|
-
<View style={styles.container}>
|
|
157
|
-
{Platform.OS === 'ios' ? (
|
|
158
|
-
<WebView
|
|
159
|
-
injectedJavaScriptObject={properties}
|
|
160
|
-
// When you are running this in development iOS doesn't allow request from hosts with invalid https certificates.
|
|
161
|
-
// If you try ngrok with the local esbuild server it will fail because ngrok requests in https and the dev server rejects http requests.
|
|
162
|
-
// Easiest solution is to use the production URL with test data.
|
|
163
|
-
// If that's not possible, you can also upload the assistant built assets to a different folder in assets.duffel and work with that.
|
|
164
|
-
source={{ uri: assistantIframeUrl }}
|
|
165
|
-
onMessage={handleMessage}
|
|
166
|
-
onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
|
|
167
|
-
// Always fetch the latest assistant bundle. The iframe.html shell
|
|
168
|
-
// references `./iframe-app.js` by a stable path, so without this
|
|
169
|
-
// the system WebView can serve a stale bundle for a long time.
|
|
170
|
-
cacheEnabled={false}
|
|
171
|
-
/>
|
|
172
|
-
) : (
|
|
173
|
-
<WebView
|
|
174
|
-
// Required for Android
|
|
175
|
-
injectedJavaScript={`
|
|
176
|
-
postMessage(
|
|
177
|
-
{
|
|
178
|
-
type: "duffel-assistant-open",
|
|
179
|
-
properties: ${JSON.stringify(properties)},
|
|
180
|
-
},
|
|
181
|
-
"*",
|
|
182
|
-
);
|
|
183
|
-
true;`}
|
|
184
|
-
source={{ uri: assistantIframeUrl }}
|
|
185
|
-
onMessage={handleMessage}
|
|
186
|
-
onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
|
|
187
|
-
// Always fetch the latest assistant bundle. The iframe.html shell
|
|
188
|
-
// references `./iframe-app.js` by a stable path, so without this
|
|
189
|
-
// the system WebView can serve a stale bundle for a long time.
|
|
190
|
-
cacheEnabled={false}
|
|
191
|
-
cacheMode="LOAD_NO_CACHE"
|
|
192
|
-
/>
|
|
193
|
-
)}
|
|
194
|
-
</View>
|
|
195
|
-
</Modal>
|
|
196
|
-
);
|
|
80
|
+
return <DuffelAssistantForiOS {...sanitizedProps} />;
|
|
197
81
|
};
|
|
198
|
-
|
|
199
|
-
const styles = StyleSheet.create({
|
|
200
|
-
container: {
|
|
201
|
-
height: '100%',
|
|
202
|
-
width: '100%',
|
|
203
|
-
paddingHorizontal: 12,
|
|
204
|
-
paddingTop: 50,
|
|
205
|
-
paddingBottom: 30,
|
|
206
|
-
},
|
|
207
|
-
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { assistantIframeUrl } from './assistantIframeUrl';
|
|
2
|
+
|
|
3
|
+
export const isAssistantIframeUrl = (url: string) => {
|
|
4
|
+
try {
|
|
5
|
+
const parsedUrl = new URL(url);
|
|
6
|
+
|
|
7
|
+
return `${parsedUrl.origin}${parsedUrl.pathname}` === assistantIframeUrl;
|
|
8
|
+
} catch (error) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const isHttpUrl = (url: string) => /^https?:\/\//.test(url);
|