cobrowse-sdk-react-native 2.15.0 → 2.16.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 (93) hide show
  1. package/.github/workflows/lint.yaml +0 -2
  2. package/android/src/main/java/io/cobrowse/reactnative/CobrowseIOModule.java +23 -0
  3. package/ios/RCTCobrowseIO.m +4 -0
  4. package/js/CBIOBroadcastPickerView.tsx +15 -0
  5. package/js/CobrowseAccessibilityService.ts +15 -0
  6. package/js/CobrowseIO.ts +156 -0
  7. package/js/CobrowseView.tsx +150 -0
  8. package/js/{Redacted.js → Redacted.tsx} +3 -2
  9. package/js/Session.ts +128 -0
  10. package/js/SessionControl.ts +38 -0
  11. package/js/Unredacted.tsx +106 -0
  12. package/js/{index.js → index.ts} +1 -0
  13. package/js/{useSession.js → useSession.ts} +5 -4
  14. package/lib/commonjs/CBIOBroadcastPickerView.js +15 -0
  15. package/lib/commonjs/CBIOBroadcastPickerView.js.map +1 -0
  16. package/lib/commonjs/CobrowseAccessibilityService.js +22 -0
  17. package/lib/commonjs/CobrowseAccessibilityService.js.map +1 -0
  18. package/lib/commonjs/CobrowseIO.js +149 -0
  19. package/lib/commonjs/CobrowseIO.js.map +1 -0
  20. package/lib/commonjs/CobrowseView.js +146 -0
  21. package/lib/commonjs/CobrowseView.js.map +1 -0
  22. package/lib/commonjs/Redacted.js +14 -0
  23. package/lib/commonjs/Redacted.js.map +1 -0
  24. package/lib/commonjs/Session.js +100 -0
  25. package/lib/commonjs/Session.js.map +1 -0
  26. package/lib/commonjs/SessionControl.js +39 -0
  27. package/lib/commonjs/SessionControl.js.map +1 -0
  28. package/lib/commonjs/Unredacted.js +82 -0
  29. package/lib/commonjs/Unredacted.js.map +1 -0
  30. package/lib/commonjs/index.js +77 -0
  31. package/lib/commonjs/index.js.map +1 -0
  32. package/lib/commonjs/useSession.js +35 -0
  33. package/lib/commonjs/useSession.js.map +1 -0
  34. package/lib/module/CBIOBroadcastPickerView.js +8 -0
  35. package/lib/module/CBIOBroadcastPickerView.js.map +1 -0
  36. package/lib/module/CobrowseAccessibilityService.js +13 -0
  37. package/lib/module/CobrowseAccessibilityService.js.map +1 -0
  38. package/lib/module/CobrowseIO.js +141 -0
  39. package/lib/module/CobrowseIO.js.map +1 -0
  40. package/lib/module/CobrowseView.js +136 -0
  41. package/lib/module/CobrowseView.js.map +1 -0
  42. package/lib/module/Redacted.js +7 -0
  43. package/lib/module/Redacted.js.map +1 -0
  44. package/lib/module/Session.js +93 -0
  45. package/lib/module/Session.js.map +1 -0
  46. package/lib/module/SessionControl.js +31 -0
  47. package/lib/module/SessionControl.js.map +1 -0
  48. package/lib/module/Unredacted.js +71 -0
  49. package/lib/module/Unredacted.js.map +1 -0
  50. package/lib/module/index.js +9 -0
  51. package/lib/module/index.js.map +1 -0
  52. package/lib/module/useSession.js +28 -0
  53. package/lib/module/useSession.js.map +1 -0
  54. package/lib/typescript/CBIOBroadcastPickerView.d.ts +8 -0
  55. package/lib/typescript/CBIOBroadcastPickerView.d.ts.map +1 -0
  56. package/lib/typescript/CobrowseAccessibilityService.d.ts +8 -0
  57. package/lib/typescript/CobrowseAccessibilityService.d.ts.map +1 -0
  58. package/lib/typescript/CobrowseIO.d.ts +30 -0
  59. package/lib/typescript/CobrowseIO.d.ts.map +1 -0
  60. package/lib/typescript/CobrowseView.d.ts +25 -0
  61. package/lib/typescript/CobrowseView.d.ts.map +1 -0
  62. package/lib/typescript/Redacted.d.ts +4 -0
  63. package/lib/typescript/Redacted.d.ts.map +1 -0
  64. package/lib/typescript/Session.d.ts +42 -0
  65. package/lib/typescript/Session.d.ts.map +1 -0
  66. package/lib/typescript/SessionControl.d.ts +17 -0
  67. package/lib/typescript/SessionControl.d.ts.map +1 -0
  68. package/lib/typescript/Unredacted.d.ts +10 -0
  69. package/lib/typescript/Unredacted.d.ts.map +1 -0
  70. package/lib/typescript/index.d.ts +10 -0
  71. package/lib/typescript/index.d.ts.map +1 -0
  72. package/lib/typescript/useSession.d.ts +3 -0
  73. package/lib/typescript/useSession.d.ts.map +1 -0
  74. package/package.json +25 -4
  75. package/tsconfig.build.json +5 -0
  76. package/tsconfig.json +28 -0
  77. package/js/CBIOBroadcastPickerView.js +0 -11
  78. package/js/CobrowseAccessibilityService.js +0 -11
  79. package/js/CobrowseIO.js +0 -145
  80. package/js/CobrowseView.js +0 -129
  81. package/js/Session.js +0 -107
  82. package/js/SessionControl.js +0 -26
  83. package/js/Unredacted.js +0 -82
  84. package/ts/CBIOBroadcastPickerView.d.ts +0 -8
  85. package/ts/CobrowseAccessibilityService.d.ts +0 -6
  86. package/ts/CobrowseIO.d.ts +0 -29
  87. package/ts/CobrowseView.d.ts +0 -20
  88. package/ts/Redacted.d.ts +0 -8
  89. package/ts/Session.d.ts +0 -58
  90. package/ts/SessionControl.d.ts +0 -9
  91. package/ts/Unredacted.d.ts +0 -16
  92. package/ts/index.d.ts +0 -7
  93. package/ts/useSession.d.ts +0 -3
@@ -18,5 +18,3 @@ jobs:
18
18
  - run: npm ci
19
19
 
20
20
  - run: npm run lint
21
-
22
- - run: npm run test:types
@@ -79,6 +79,19 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule {
79
79
  CobrowseIO.instance().customData(customData.toHashMap());
80
80
  }
81
81
 
82
+ @ReactMethod
83
+ public void capabilities(ReadableArray capabilities) {
84
+ String[] capabilitiesArray = new String[capabilities.size()];
85
+ for (int i = 0; i < capabilities.size(); i++) {
86
+ try {
87
+ capabilitiesArray[i] = capabilities.getString(i);
88
+ } catch (UnexpectedNativeTypeException e) {
89
+ Log.w("CobrowseIO", "Failed to process capabilities: " + e.getMessage());
90
+ }
91
+ }
92
+ CobrowseIO.instance().capabilities(capabilitiesArray);
93
+ }
94
+
82
95
  @ReactMethod
83
96
  public void deviceToken(String token) {
84
97
  if (getReactApplicationContext().getCurrentActivity() != null)
@@ -240,4 +253,14 @@ public class CobrowseIOModule extends ReactContextBaseJavaModule {
240
253
 
241
254
  CobrowseIO.instance().setDelegate(delegatesWithSessionControls);
242
255
  }
256
+
257
+ @ReactMethod
258
+ public void addListener(String eventName) {
259
+ // Keep: Required for RN built in Event Emitter Calls.
260
+ }
261
+
262
+ @ReactMethod
263
+ public void removeListeners(Integer count) {
264
+ // Keep: Required for RN built in Event Emitter Calls.
265
+ }
243
266
  }
@@ -161,6 +161,10 @@ RCT_EXPORT_METHOD(customData: (NSDictionary*) customData) {
161
161
  CobrowseIO.instance.customData = customData;
162
162
  }
163
163
 
164
+ RCT_EXPORT_METHOD(capabilities: (NSArray*) capabilities) {
165
+ CobrowseIO.instance.capabilities = capabilities;
166
+ }
167
+
164
168
  RCT_EXPORT_METHOD(deviceToken: (NSString*) token) {
165
169
  CobrowseIO.instance.device.token = [token dataUsingEncoding:NSUTF16StringEncoding];
166
170
  }
@@ -0,0 +1,15 @@
1
+ import React from 'react'
2
+ import { Platform, requireNativeComponent, type ViewProps } from 'react-native'
3
+
4
+ const CBIOBroadcastPickerViewNativeView = requireNativeComponent(
5
+ 'CBIOBroadcastPickerView'
6
+ )
7
+
8
+ type CBIOBroadcastPickerViewProps = {
9
+ buttonColor: string
10
+ } & ViewProps
11
+
12
+ export default function CBIOBroadcastPickerView (props: CBIOBroadcastPickerViewProps): JSX.Element | null {
13
+ // this native component is only available on iOS
14
+ return Platform.OS === 'ios' ? <CBIOBroadcastPickerViewNativeView {...props} /> : null
15
+ }
@@ -0,0 +1,15 @@
1
+ import { NativeModules } from 'react-native'
2
+ const CobrowseIONative = NativeModules.CobrowseIO
3
+
4
+ export function showSetup (): void {
5
+ return CobrowseIONative.accessibilityServiceShowSetup()
6
+ }
7
+
8
+ export function isRunning (): boolean {
9
+ return CobrowseIONative.accessibilityServiceIsRunning()
10
+ }
11
+
12
+ export default {
13
+ showSetup,
14
+ isRunning
15
+ }
@@ -0,0 +1,156 @@
1
+ import { Platform, Alert, NativeEventEmitter, EmitterSubscription, NativeModules } from 'react-native'
2
+ import Session, { type CobrowseSessionEvents, type SessionLike } from './Session'
3
+ const CobrowseIONative = NativeModules.CobrowseIO
4
+
5
+ const emitter = new NativeEventEmitter(CobrowseIONative)
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-extraneous-class
8
+ export default class CobrowseIO {
9
+ private static sessionRequestShown = false
10
+ private static remoteControlRequestShown = false
11
+
12
+ /** @deprecated */
13
+ static get SESSION_UPDATED (): 'session.updated' {
14
+ return 'session.updated' as const
15
+ }
16
+
17
+ /** @deprecated */
18
+ static get SESSION_ENDED (): 'session.ended' {
19
+ return 'session.ended' as const
20
+ }
21
+
22
+ static handleSessionRequest (session: Session): void {
23
+ if (this.sessionRequestShown) return
24
+ this.sessionRequestShown = true
25
+ Alert.alert(
26
+ 'Support Request',
27
+ 'A support agent would like to use this app with you. Do you accept?',
28
+ [{
29
+ text: 'Reject',
30
+ onPress: () => {
31
+ this.sessionRequestShown = false
32
+ void session.end()
33
+ },
34
+ style: 'cancel'
35
+ }, {
36
+ text: 'Accept',
37
+ onPress: () => {
38
+ this.sessionRequestShown = false
39
+ void session.activate()
40
+ }
41
+ }], { cancelable: false })
42
+ }
43
+
44
+ static handleRemoteControlRequest (session: Session): void {
45
+ if (this.remoteControlRequestShown) return
46
+ this.remoteControlRequestShown = true
47
+ Alert.alert(
48
+ 'Remote Control Request',
49
+ 'A support agent would like to take remote control of this app. Do you accept?',
50
+ [{
51
+ text: 'Reject',
52
+ onPress: () => {
53
+ this.remoteControlRequestShown = false
54
+ void session.setRemoteControl('rejected')
55
+ },
56
+ style: 'cancel'
57
+ }, {
58
+ text: 'Accept',
59
+ onPress: () => {
60
+ this.remoteControlRequestShown = false
61
+ void session.setRemoteControl('on')
62
+ }
63
+ }], { cancelable: false })
64
+ }
65
+
66
+ static addListener (event: CobrowseSessionEvents | 'session.requested', cb: (session: Session) => void): EmitterSubscription {
67
+ return emitter.addListener(event, (session: SessionLike) => cb(new Session(session)))
68
+ }
69
+
70
+ static start (): void {
71
+ CobrowseIONative.start()
72
+ }
73
+
74
+ static stop (): void {
75
+ CobrowseIONative.stop()
76
+ }
77
+
78
+ // eslint-disable-next-line accessor-pairs
79
+ static set api (api: string) {
80
+ CobrowseIONative.api(api)
81
+ }
82
+
83
+ // eslint-disable-next-line accessor-pairs
84
+ static set license (license: string) {
85
+ CobrowseIONative.license(license)
86
+ }
87
+
88
+ // eslint-disable-next-line accessor-pairs
89
+ static set customData (customData: Record<string, unknown>) {
90
+ CobrowseIONative.customData(customData)
91
+ }
92
+
93
+ // eslint-disable-next-line accessor-pairs
94
+ static set capabilities (capabilities: Array<'drawing' | 'full_device' | 'keypress' | 'laser' | 'pointer'>) {
95
+ CobrowseIONative.capabilities(capabilities)
96
+ }
97
+
98
+ // eslint-disable-next-line accessor-pairs
99
+ static set deviceToken (token: string) {
100
+ CobrowseIONative.deviceToken(token)
101
+ }
102
+
103
+ static currentSession (): Session | null {
104
+ return CobrowseIONative.currentSession().then((session: SessionLike | null) => (session != null) ? new Session(session) : null)
105
+ }
106
+
107
+ static createSession (): Session | null {
108
+ return CobrowseIONative.createSession().then((session: SessionLike | null) => (session != null) ? new Session(session) : null)
109
+ }
110
+
111
+ static getSession (codeOrId: string): Session | null {
112
+ return CobrowseIONative.getSession(codeOrId).then((session: SessionLike | null) => (session != null) ? new Session(session) : null)
113
+ }
114
+
115
+ /** @deprecated */
116
+ static activateSession (): Session | null {
117
+ return CobrowseIONative.activateSession().then((session: SessionLike | null) => (session != null) ? new Session(session) : null)
118
+ }
119
+
120
+ /** @deprecated */
121
+ static endSession (): void {
122
+ return CobrowseIONative.endSession()
123
+ }
124
+
125
+ static showSessionControls: Boolean | null = null
126
+ static handleFullDeviceRequest: null | ((s: Session) => void) = null
127
+ }
128
+
129
+ // the session.requested event is considered internal, it should
130
+ // not be used outside these bindings
131
+ CobrowseIO.addListener('session.requested', (session) => {
132
+ if (CobrowseIO.showSessionControls === false) {
133
+ CobrowseIONative.overwriteSessionIndicator()
134
+ }
135
+
136
+ if (Platform.OS === 'ios' && (CobrowseIO.handleFullDeviceRequest != null)) {
137
+ CobrowseIONative.overwriteFullControlUI()
138
+ }
139
+
140
+ CobrowseIO.handleSessionRequest(session)
141
+ })
142
+
143
+ CobrowseIO.addListener('session.updated', (session) => {
144
+ if (session.isActive() && session.remote_control === 'requested') {
145
+ CobrowseIO.handleRemoteControlRequest(session)
146
+ }
147
+
148
+ if (session.isActive() && session.full_device_state === 'requested') {
149
+ if (CobrowseIO.handleFullDeviceRequest != null) {
150
+ CobrowseIO.handleFullDeviceRequest(session)
151
+ } else if (Platform.OS === 'android') {
152
+ // accept the incoming connection by default
153
+ void session.setFullDevice('on')
154
+ }
155
+ }
156
+ })
@@ -0,0 +1,150 @@
1
+ import React, { Component } from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ ActivityIndicator,
6
+ TouchableOpacity,
7
+ StyleSheet,
8
+ EmitterSubscription
9
+ } from 'react-native'
10
+ import CobrowseIO from './CobrowseIO'
11
+ import type Session from './Session'
12
+
13
+ const styles = StyleSheet.create({
14
+ container: {
15
+ flex: 1,
16
+ justifyContent: 'flex-end'
17
+ },
18
+ text: {
19
+ textAlign: 'center',
20
+ margin: 15,
21
+ fontSize: 15,
22
+ lineHeight: 20
23
+ },
24
+ code: {
25
+ fontSize: 29,
26
+ padding: 20,
27
+ fontWeight: 'bold',
28
+ textAlign: 'center'
29
+ },
30
+ button: {
31
+ color: 'rgb(0, 122, 255)',
32
+ fontSize: 18,
33
+ margin: 10
34
+ }
35
+ })
36
+
37
+ interface CobrowseViewProps {
38
+ // @deprecated
39
+ license?: string
40
+ onEnded?: () => void
41
+ }
42
+
43
+ interface CobrowseViewState {
44
+ error: Error | null
45
+ session: Session | null
46
+ }
47
+
48
+ export default class CobrowseView extends Component<CobrowseViewProps, CobrowseViewState> {
49
+ private _updateListener: EmitterSubscription | null = null
50
+ private _endListener: EmitterSubscription | null = null
51
+
52
+ constructor (props: CobrowseViewProps) {
53
+ super(props)
54
+ this.state = { error: null, session: null }
55
+ }
56
+
57
+ async componentDidMount (): Promise<void> {
58
+ if (this.props.license != null) {
59
+ console.warn('Passing license to view is deprecated. Use CobrowseIO.license = "..." instead')
60
+ CobrowseIO.license = this.props.license
61
+ }
62
+
63
+ try {
64
+ const current = await CobrowseIO.currentSession()
65
+ if (current != null) this.setState({ session: current })
66
+ else {
67
+ const session = await CobrowseIO.createSession()
68
+ this.setState({ session })
69
+ }
70
+ } catch (error) {
71
+ this.setState({ error: error as Error })
72
+ }
73
+
74
+ this._updateListener = CobrowseIO.addListener('session.updated', (session) => {
75
+ this.setState({ session })
76
+ })
77
+ this._endListener = CobrowseIO.addListener('session.ended', () => {
78
+ if (this.props.onEnded != null) this.props.onEnded()
79
+ this.setState({ session: null })
80
+ })
81
+ }
82
+
83
+ componentWillUnmount (): void {
84
+ if (this._updateListener != null) this._updateListener.remove()
85
+ if (this._endListener != null) this._endListener.remove()
86
+ }
87
+
88
+ async endSession (): Promise<void> {
89
+ try {
90
+ const { session } = this.state
91
+
92
+ if (session == null) {
93
+ throw new Error('No session to end')
94
+ }
95
+
96
+ await session.end()
97
+ this.setState({ session: null })
98
+ } catch (error) {
99
+ this.setState({ error: error as Error })
100
+ }
101
+ }
102
+
103
+ renderError (error: Error): JSX.Element {
104
+ return (
105
+ <Text style={[styles.text]}>{error.message}</Text>
106
+ )
107
+ }
108
+
109
+ renderCode (): JSX.Element {
110
+ let code = this.state.session?.code
111
+ if (code != null) code = code.substr(0, 3) + '-' + code.substr(3)
112
+ return (
113
+ <View>
114
+ <Text style={[styles.code, { opacity: code != null ? 1 : 0.2 }]}>{code ?? '000-000'}</Text>
115
+ <Text style={[styles.text]}>Provide this code to your support agent to begin screen sharing.</Text>
116
+ <ActivityIndicator />
117
+ </View>
118
+ )
119
+ }
120
+
121
+ renderManageSession (): JSX.Element {
122
+ return (
123
+ <View>
124
+ <Text style={[styles.text]}>You're sharing screens from this app with a support agent.</Text>
125
+ <TouchableOpacity onPress={() => this.endSession}>
126
+ <Text style={[styles.text, styles.button]}>End Session</Text>
127
+ </TouchableOpacity>
128
+ </View>
129
+ )
130
+ }
131
+
132
+ renderContent (): JSX.Element {
133
+ const { error, session } = this.state
134
+ if (error != null) {
135
+ return this.renderError(error)
136
+ } else if ((session == null) || (session.state === 'pending' || session.state === 'authorizing')) {
137
+ return this.renderCode()
138
+ } else {
139
+ return this.renderManageSession()
140
+ }
141
+ }
142
+
143
+ render (): JSX.Element {
144
+ return (
145
+ <View style={styles.container}>
146
+ {this.renderContent()}
147
+ </View>
148
+ )
149
+ }
150
+ }
@@ -1,7 +1,8 @@
1
1
  import React from 'react'
2
- import { requireNativeComponent } from 'react-native'
2
+ import { requireNativeComponent, type ViewProps } from 'react-native'
3
+
3
4
  const CBIOCobrowseRedacted = requireNativeComponent('CBIOCobrowseRedacted')
4
5
 
5
- module.exports = function (props) {
6
+ export default function Redacted (props: ViewProps): JSX.Element {
6
7
  return <CBIOCobrowseRedacted {...props}>{props.children}</CBIOCobrowseRedacted>
7
8
  }
package/js/Session.ts ADDED
@@ -0,0 +1,128 @@
1
+ import { EmitterSubscription, NativeEventEmitter, NativeModules } from 'react-native'
2
+ const CobrowseIONative = NativeModules.CobrowseIO
3
+
4
+ const emitter = new NativeEventEmitter(CobrowseIONative)
5
+
6
+ export type SessionEvent = 'updated' | 'ended'
7
+ export type CobrowseSessionEvents = `session.${SessionEvent}`
8
+ export type SessionState = 'active' | 'authorizing' | 'ended' | 'pending'
9
+ export type RemoteControlState = 'on' | 'requested' | 'rejected' | 'off'
10
+ export type FullDeviceState = 'on' | 'requested' | 'rejected' | 'off'
11
+
12
+ export interface Agent {
13
+ id: string
14
+ name: string
15
+ }
16
+
17
+ export interface SessionLike {
18
+ id: string
19
+ code: string
20
+ state: SessionState
21
+ full_device: boolean
22
+ remote_control: RemoteControlState
23
+ agent: Agent
24
+ full_device_state: FullDeviceState
25
+ }
26
+
27
+ export default class Session {
28
+ constructor (private session: SessionLike | Record<string, never> = {}) {
29
+ this._listen()
30
+ }
31
+
32
+ private _update (session: SessionLike, onMatch?: () => void): void {
33
+ if (this.session.id === undefined || (session != null && this.session.id === session.id)) {
34
+ this.session = session
35
+ if (typeof onMatch === 'function') onMatch()
36
+ }
37
+ }
38
+
39
+ _listen (): void {
40
+ const updates = emitter.addListener('session.updated', (session: SessionLike) => this._update(session))
41
+ const ended = emitter.addListener('session.ended', (session: SessionLike) => {
42
+ this._update(session, () => {
43
+ updates.remove()
44
+ ended.remove()
45
+ })
46
+ })
47
+ }
48
+
49
+ addListener (eventType: SessionEvent, listener: (session: Session) => void): EmitterSubscription {
50
+ let mappedEventsType: CobrowseSessionEvents
51
+ // TODO: this class should extend EventEmitter and forward
52
+ // on the non-namespaced versions of these events
53
+ switch (eventType) {
54
+ case 'ended':
55
+ mappedEventsType = 'session.updated'
56
+ break
57
+ case 'updated':
58
+ mappedEventsType = 'session.ended'
59
+ break
60
+ }
61
+
62
+ return emitter.addListener(mappedEventsType ?? eventType, (session: SessionLike) => {
63
+ this._update(session, () => {
64
+ listener(this)
65
+ })
66
+ })
67
+ }
68
+
69
+ get id (): string | null {
70
+ return this.session.id
71
+ }
72
+
73
+ get code (): string | null {
74
+ return this.session.code
75
+ }
76
+
77
+ get state (): SessionState | null {
78
+ return this.session.state
79
+ }
80
+
81
+ get full_device (): boolean {
82
+ return this.session.full_device ?? false
83
+ }
84
+
85
+ get full_device_state (): FullDeviceState {
86
+ return this.session.full_device_state ?? 'off'
87
+ }
88
+
89
+ get remote_control (): RemoteControlState | null {
90
+ return this.session.remote_control
91
+ }
92
+
93
+ get agent (): Agent | null {
94
+ return this.session.agent
95
+ }
96
+
97
+ async activate (): Promise<void> {
98
+ return CobrowseIONative.activateSession().then(() => {})
99
+ }
100
+
101
+ async end (): Promise<void> {
102
+ return CobrowseIONative.endSession()
103
+ }
104
+
105
+ isActive (): boolean {
106
+ return this.session.state === 'active'
107
+ }
108
+
109
+ isAuthorizing (): boolean {
110
+ return this.session.state === 'authorizing'
111
+ }
112
+
113
+ isPending (): boolean {
114
+ return this.session.state === 'pending'
115
+ }
116
+
117
+ isEnded (): boolean {
118
+ return this.session.state === 'ended'
119
+ }
120
+
121
+ async setFullDevice (state: boolean | FullDeviceState): Promise<void> {
122
+ return CobrowseIONative.updateSession({ full_device: state })
123
+ }
124
+
125
+ async setRemoteControl (state: RemoteControlState): Promise<void> {
126
+ return CobrowseIONative.updateSession({ remote_control: state })
127
+ }
128
+ }
@@ -0,0 +1,38 @@
1
+ import React, { Component } from 'react'
2
+ import CobrowseIO from './CobrowseIO'
3
+ import type { EmitterSubscription } from 'react-native'
4
+ import type Session from './Session'
5
+
6
+ interface SessionControlProps {
7
+ children: React.ReactNode
8
+ }
9
+
10
+ interface SessionControlState {
11
+ session: Session | null
12
+ }
13
+
14
+ export default class SessionControl extends Component<SessionControlProps, SessionControlState> {
15
+ private _updateListener: EmitterSubscription | null = null
16
+
17
+ constructor (props: SessionControlProps) {
18
+ super(props)
19
+ this.state = { session: null }
20
+ }
21
+
22
+ async componentDidMount (): Promise<void> {
23
+ this.setState({ session: await CobrowseIO.currentSession() })
24
+ this._updateListener = CobrowseIO.addListener(CobrowseIO.SESSION_UPDATED, (session) => {
25
+ this.setState({ session })
26
+ })
27
+ }
28
+
29
+ componentWillUnmount (): void {
30
+ if (this._updateListener != null) this._updateListener.remove()
31
+ }
32
+
33
+ render (): React.ReactNode | null {
34
+ const { session } = this.state
35
+ if (session?.isActive() === true) return this.props.children
36
+ else return null
37
+ }
38
+ }
@@ -0,0 +1,106 @@
1
+ import React, {
2
+ forwardRef,
3
+ useMemo,
4
+ useRef,
5
+ useCallback,
6
+ useEffect,
7
+ Component as ReactComponentType,
8
+ PropsWithoutRef,
9
+ type ForwardRefExoticComponent,
10
+ type RefAttributes,
11
+ type ComponentClass,
12
+ type Ref,
13
+ type PropsWithChildren,
14
+ type ForwardedRef
15
+ } from 'react'
16
+ import { ScrollView, Text, View, SectionList, FlatList, Image, findNodeHandle, NativeModules } from 'react-native'
17
+ import { throttle } from 'lodash'
18
+
19
+ const CobrowseIONative = NativeModules.CobrowseIO
20
+
21
+ type RefHandlingComponent<T, P> =
22
+ | ComponentClass<P & { ref?: Ref<T> }, any>
23
+ | ForwardRefExoticComponent<PropsWithChildren<P> & RefAttributes<T>>
24
+
25
+ function mergeRefs<T> (refs: Array<ForwardedRef<T>>): Ref<T> {
26
+ return (value) => {
27
+ refs.forEach((ref) => {
28
+ if (typeof ref === 'function') {
29
+ ref(value)
30
+ } else if (ref != null) {
31
+ ref.current = value
32
+ }
33
+ })
34
+ }
35
+ }
36
+
37
+ const unredactedTags = new Set()
38
+ const sendUnredactionUpdates = throttle(() => {
39
+ CobrowseIONative.setUnredactedTags([...unredactedTags])
40
+ }, 50, { leading: false })
41
+
42
+ function removeUnredactedView (view: number | null): void {
43
+ if (view != null) {
44
+ unredactedTags.delete(view)
45
+ sendUnredactionUpdates()
46
+ }
47
+ }
48
+
49
+ export function useUnredaction<C extends ComponentClass | ReactComponentType = View | ScrollView | Text | Image | FlatList | SectionList> (shouldWarnUnhandledRefs = true, componentName = ''): ForwardedRef<C> {
50
+ const ref = useRef<number | null>(null)
51
+
52
+ const setRef = useCallback((node: C | null) => {
53
+ let hasRemovedRef = false
54
+ if (ref.current != null) {
55
+ hasRemovedRef = true
56
+ removeUnredactedView(ref.current)
57
+ }
58
+
59
+ if (node != null) {
60
+ const view = findNodeHandle(node)
61
+
62
+ if (view != null) {
63
+ unredactedTags.add(view)
64
+ sendUnredactionUpdates()
65
+
66
+ ref.current = view
67
+ } else {
68
+ console.warn(`Failed to apply unredact() to ${componentName} due to view not found`)
69
+ }
70
+ } else if (!hasRemovedRef) {
71
+ console.warn(
72
+ `Failed to apply unredact() to ${componentName} due to null node handle – make sure you are forwarding refs`
73
+ )
74
+ }
75
+ }, [])
76
+
77
+ useEffect(() => {
78
+ if (shouldWarnUnhandledRefs && ref.current == null) {
79
+ console.warn(
80
+ `Failed to apply unredact() to ${componentName} due to null node handle – make sure the setRef function is called with the ref`
81
+ )
82
+ }
83
+
84
+ return () => removeUnredactedView(ref.current)
85
+ }, [])
86
+
87
+ return setRef
88
+ }
89
+
90
+ export function unredact<T, P extends {}> (
91
+ Component: RefHandlingComponent<T, P>
92
+ ): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> {
93
+ return forwardRef<T, P>(
94
+ function ComponentFromHOC (props, ref) {
95
+ const localRef = useUnredaction(true, Component?.displayName) as ForwardedRef<T>
96
+ const refs = useMemo(() => mergeRefs([localRef, ref]), [localRef, ref])
97
+
98
+ return (
99
+ <Component {...(props)} ref={refs} />
100
+ )
101
+ }
102
+ )
103
+ }
104
+
105
+ const Unredacted = unredact(View)
106
+ export default Unredacted
@@ -6,3 +6,4 @@ export { default as SessionControl } from './SessionControl'
6
6
  export { default as CobrowseAccessibilityService } from './CobrowseAccessibilityService'
7
7
  export { default as CBIOBroadcastPickerView } from './CBIOBroadcastPickerView'
8
8
  export { useSession } from './useSession'
9
+ export type { default as Session, CobrowseSessionEvents } from './Session'