cozy-ui 116.0.0 → 117.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/react/AppLinker/Readme.md +3 -31
- package/react/AppLinker/__snapshots__/index.spec.jsx.snap +0 -12
- package/react/AppLinker/index.jsx +8 -144
- package/react/AppLinker/index.spec.jsx +1 -94
- package/react/AppLinker/native.config.js +0 -14
- package/transpiled/react/AppLinker/index.js +10 -179
- package/transpiled/react/AppLinker/native.config.js +0 -13
- package/react/AppLinker/__snapshots__/index.deprecated.spec.jsx.snap +0 -25
- package/react/AppLinker/expiringMemoize.js +0 -13
- package/react/AppLinker/index.deprecated.spec.jsx +0 -162
- package/transpiled/react/AppLinker/expiringMemoize.js +0 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [117.0.0](https://github.com/cozy/cozy-ui/compare/v116.0.0...v117.0.0) (2025-01-13)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* Remove cordova stuff from AppLinker ([add9fbb](https://github.com/cozy/cozy-ui/commit/add9fbb))
|
|
7
|
+
* Remove deprecated slug prop from AppLinker ([a3d47e0](https://github.com/cozy/cozy-ui/commit/a3d47e0))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* Use app={{ slug: 'drive' }} instead of slug='drive' as
|
|
13
|
+
AppLinker prop. Also, `name` is not passed anymore as render prop.
|
|
14
|
+
|
|
1
15
|
# [116.0.0](https://github.com/cozy/cozy-ui/compare/v115.1.0...v116.0.0) (2025-01-07)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
Render-props component that provides onClick/href handler to
|
|
2
2
|
apply to an anchor that needs to open an app.
|
|
3
3
|
|
|
4
|
-
If the app is known to Cozy (for example Drive or Banks), and
|
|
5
|
-
the user has installed it on its device, the native app will
|
|
6
|
-
be opened.
|
|
7
|
-
|
|
8
4
|
The app's manifest can be set in the `app` prop. Then, in a
|
|
9
5
|
ReactNative environment, the AppLinker will be able to send
|
|
10
6
|
`openApp` message to the native environment with this `app`
|
|
11
|
-
data.
|
|
12
|
-
needed to open the native app ([more info](https://github.com/cozy/cozy-stack/blob/master/docs/apps.md#mobile))
|
|
7
|
+
data.
|
|
13
8
|
|
|
14
9
|
Handles several cases:
|
|
15
10
|
|
|
@@ -28,32 +23,9 @@ const app = {
|
|
|
28
23
|
};
|
|
29
24
|
|
|
30
25
|
<AppLinker app={app} href='http://dalailama-banks.mycozy.cloud'>{
|
|
31
|
-
({ onClick, href
|
|
32
|
-
<a href={href} onClick={onClick}>
|
|
33
|
-
Open { name }
|
|
34
|
-
</a>
|
|
35
|
-
)
|
|
36
|
-
}</AppLinker>
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### Exemple with mobile data
|
|
40
|
-
|
|
41
|
-
```jsx
|
|
42
|
-
import AppLinker from 'cozy-ui/transpiled/react/AppLinker';
|
|
43
|
-
|
|
44
|
-
const app = {
|
|
45
|
-
slug: 'passwords',
|
|
46
|
-
mobile: {
|
|
47
|
-
schema: 'cozypass://',
|
|
48
|
-
id_playstore: 'io.cozy.pass',
|
|
49
|
-
id_appstore: 'cozy-pass/id1502262449'
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
<AppLinker app={app} href='http://dalailama-passwords.mycozy.cloud'>{
|
|
54
|
-
({ onClick, href, name }) => (
|
|
26
|
+
({ onClick, href }) => (
|
|
55
27
|
<a href={href} onClick={onClick}>
|
|
56
|
-
Open
|
|
28
|
+
Open
|
|
57
29
|
</a>
|
|
58
30
|
)
|
|
59
31
|
}</AppLinker>
|
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
|
-
exports[`app icon should not crash if no href 1`] = `
|
|
4
|
-
<div>
|
|
5
|
-
<div>
|
|
6
|
-
<a>
|
|
7
|
-
Open
|
|
8
|
-
Cozy Drive
|
|
9
|
-
</a>
|
|
10
|
-
</div>
|
|
11
|
-
</div>
|
|
12
|
-
`;
|
|
13
|
-
|
|
14
3
|
exports[`app icon should render correctly 1`] = `
|
|
15
4
|
<div>
|
|
16
5
|
<div>
|
|
@@ -18,7 +7,6 @@ exports[`app icon should render correctly 1`] = `
|
|
|
18
7
|
href="https://fake.link"
|
|
19
8
|
>
|
|
20
9
|
Open
|
|
21
|
-
Cozy Drive
|
|
22
10
|
</a>
|
|
23
11
|
</div>
|
|
24
12
|
</div>
|
|
@@ -2,45 +2,25 @@ import PropTypes from 'prop-types'
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
4
|
import { withClient } from 'cozy-client'
|
|
5
|
-
import {
|
|
6
|
-
checkApp,
|
|
7
|
-
startApp,
|
|
8
|
-
isMobileApp,
|
|
9
|
-
isMobile,
|
|
10
|
-
openDeeplinkOrRedirect,
|
|
11
|
-
isAndroid,
|
|
12
|
-
isFlagshipApp
|
|
13
|
-
} from 'cozy-device-helper'
|
|
5
|
+
import { isFlagshipApp } from 'cozy-device-helper'
|
|
14
6
|
import { WebviewContext } from 'cozy-intent'
|
|
15
7
|
import logger from 'cozy-logger'
|
|
16
8
|
|
|
17
|
-
import expiringMemoize from './expiringMemoize'
|
|
18
9
|
import {
|
|
19
10
|
generateUniversalLink,
|
|
20
11
|
generateWebLink,
|
|
21
12
|
getUniversalLinkDomain
|
|
22
13
|
} from './native'
|
|
23
|
-
import { NATIVE_APP_INFOS } from './native.config'
|
|
24
|
-
|
|
25
|
-
const expirationDelay = 10 * 1000
|
|
26
|
-
const memoizedCheckApp = expiringMemoize(
|
|
27
|
-
appInfo => checkApp(appInfo).catch(() => false),
|
|
28
|
-
expirationDelay,
|
|
29
|
-
appInfo => appInfo.appId
|
|
30
|
-
)
|
|
31
14
|
|
|
32
15
|
export class AppLinker extends React.Component {
|
|
33
16
|
static contextType = WebviewContext
|
|
34
17
|
|
|
35
18
|
state = {
|
|
36
|
-
|
|
37
|
-
isFetchingAppInfo: false
|
|
19
|
+
imgRef: null
|
|
38
20
|
}
|
|
39
21
|
|
|
40
22
|
constructor(props) {
|
|
41
23
|
super(props)
|
|
42
|
-
|
|
43
|
-
this.imgRef = null
|
|
44
24
|
}
|
|
45
25
|
|
|
46
26
|
setImgRef = img => {
|
|
@@ -48,44 +28,14 @@ export class AppLinker extends React.Component {
|
|
|
48
28
|
this.setState({ imgRef: this.imgRef })
|
|
49
29
|
}
|
|
50
30
|
|
|
51
|
-
componentDidMount() {
|
|
52
|
-
if (isMobileApp()) {
|
|
53
|
-
this.checkAppAvailability()
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async checkAppAvailability() {
|
|
58
|
-
const slug = AppLinker.getSlug(this.props)
|
|
59
|
-
const appInfo = NATIVE_APP_INFOS[slug]
|
|
60
|
-
if (appInfo) {
|
|
61
|
-
const nativeAppIsAvailable = Boolean(await memoizedCheckApp(appInfo))
|
|
62
|
-
this.setState({ nativeAppIsAvailable })
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
31
|
static getSlug(props) {
|
|
67
|
-
|
|
68
|
-
return props.app.slug
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return props.slug
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
static deprecateSlug(props) {
|
|
75
|
-
if (props.slug) {
|
|
76
|
-
console.warn(
|
|
77
|
-
`AppLinker's 'slug' prop is deprecated, please use 'app.slug' instead`
|
|
78
|
-
)
|
|
79
|
-
}
|
|
32
|
+
return props.app.slug
|
|
80
33
|
}
|
|
81
34
|
|
|
82
|
-
static getOnClickHref(props,
|
|
83
|
-
const { app, client
|
|
84
|
-
const slug = AppLinker.getSlug(props)
|
|
35
|
+
static getOnClickHref(props, context, imgRef) {
|
|
36
|
+
const { app, client } = props
|
|
85
37
|
let href = props.href
|
|
86
38
|
let onClick = null
|
|
87
|
-
const usingNativeApp = isMobileApp()
|
|
88
|
-
const appInfo = NATIVE_APP_INFOS[slug]
|
|
89
39
|
|
|
90
40
|
if (isFlagshipApp()) {
|
|
91
41
|
const { app: currentApp } = client
|
|
@@ -114,59 +64,11 @@ export class AppLinker extends React.Component {
|
|
|
114
64
|
}
|
|
115
65
|
}
|
|
116
66
|
|
|
117
|
-
if (usingNativeApp) {
|
|
118
|
-
if (nativeAppIsAvailable) {
|
|
119
|
-
// If we are on the native app and the other native app is available,
|
|
120
|
-
// we open the native app
|
|
121
|
-
onClick = AppLinker.openNativeFromNative.bind(this, props)
|
|
122
|
-
href = '#'
|
|
123
|
-
} else {
|
|
124
|
-
// If we are on a native app, but the other native app is not available
|
|
125
|
-
// we open the web link, this is done by the href prop. We still
|
|
126
|
-
// have to call the prop callback
|
|
127
|
-
onClick = AppLinker.openWeb.bind(this, props)
|
|
128
|
-
}
|
|
129
|
-
} else if (isMobile() && appInfo) {
|
|
130
|
-
// If we are on the "mobile web version", we try to open the native app
|
|
131
|
-
// if it exists with an universal links. If it fails, we redirect to the web
|
|
132
|
-
// version of the requested app
|
|
133
|
-
// Only on iOS ATM
|
|
134
|
-
if (isAndroid()) {
|
|
135
|
-
onClick = AppLinker.openNativeFromWeb.bind(this, props)
|
|
136
|
-
} else {
|
|
137
|
-
// Since generateUniversalLink can rise an error, let's catch it to not crash
|
|
138
|
-
// all the page.
|
|
139
|
-
try {
|
|
140
|
-
href = generateUniversalLink({ slug, nativePath, fallbackUrl: href })
|
|
141
|
-
} catch (err) {
|
|
142
|
-
console.error(err)
|
|
143
|
-
href = '#'
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
67
|
return {
|
|
149
68
|
href,
|
|
150
69
|
onClick
|
|
151
70
|
}
|
|
152
71
|
}
|
|
153
|
-
static openNativeFromWeb(props, ev) {
|
|
154
|
-
const { href, nativePath, onAppSwitch } = props
|
|
155
|
-
const slug = AppLinker.getSlug(props)
|
|
156
|
-
const appInfo = NATIVE_APP_INFOS[slug]
|
|
157
|
-
|
|
158
|
-
if (ev) {
|
|
159
|
-
ev.preventDefault()
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
AppLinker.onAppSwitch(onAppSwitch)
|
|
163
|
-
openDeeplinkOrRedirect(
|
|
164
|
-
appInfo.uri + (nativePath === '/' ? '' : nativePath),
|
|
165
|
-
function () {
|
|
166
|
-
window.location.href = href
|
|
167
|
-
}
|
|
168
|
-
)
|
|
169
|
-
}
|
|
170
72
|
|
|
171
73
|
static onAppSwitch(onAppSwitchFn) {
|
|
172
74
|
if (typeof onAppSwitchFn === 'function') {
|
|
@@ -174,38 +76,20 @@ export class AppLinker extends React.Component {
|
|
|
174
76
|
}
|
|
175
77
|
}
|
|
176
78
|
|
|
177
|
-
static openNativeFromNative(props, ev) {
|
|
178
|
-
const { onAppSwitch } = props
|
|
179
|
-
const slug = AppLinker.getSlug(props)
|
|
180
|
-
if (ev) {
|
|
181
|
-
ev.preventDefault()
|
|
182
|
-
}
|
|
183
|
-
const appInfo = NATIVE_APP_INFOS[slug]
|
|
184
|
-
AppLinker.onAppSwitch(onAppSwitch)
|
|
185
|
-
startApp(appInfo).catch(err => {
|
|
186
|
-
console.error('AppLinker: Could not open native app', err)
|
|
187
|
-
})
|
|
188
|
-
}
|
|
189
|
-
|
|
190
79
|
static openWeb(props) {
|
|
191
80
|
AppLinker.onAppSwitch(props.onAppSwitch)
|
|
192
81
|
}
|
|
193
82
|
|
|
194
83
|
render() {
|
|
195
84
|
const { children } = this.props
|
|
196
|
-
|
|
197
|
-
const slug = AppLinker.getSlug(this.props)
|
|
198
|
-
const { nativeAppIsAvailable } = this.state
|
|
199
|
-
const appInfo = NATIVE_APP_INFOS[slug]
|
|
85
|
+
|
|
200
86
|
const { href, onClick } = AppLinker.getOnClickHref(
|
|
201
87
|
this.props,
|
|
202
|
-
nativeAppIsAvailable,
|
|
203
88
|
this.context,
|
|
204
89
|
this.state.imgRef
|
|
205
90
|
)
|
|
206
91
|
|
|
207
92
|
return children({
|
|
208
|
-
...appInfo,
|
|
209
93
|
iconRef: this.setImgRef,
|
|
210
94
|
onClick: onClick,
|
|
211
95
|
href
|
|
@@ -213,38 +97,18 @@ export class AppLinker extends React.Component {
|
|
|
213
97
|
}
|
|
214
98
|
}
|
|
215
99
|
|
|
216
|
-
AppLinker.defaultProps = {
|
|
217
|
-
nativePath: '/'
|
|
218
|
-
}
|
|
219
100
|
AppLinker.propTypes = {
|
|
220
|
-
/** DEPRECATED: please use app.slug prop */
|
|
221
|
-
slug: PropTypes.string,
|
|
222
101
|
/*
|
|
223
102
|
Full web url : Used by default on desktop browser
|
|
224
103
|
Used as a fallback_uri on mobile web
|
|
225
104
|
*/
|
|
226
105
|
href: PropTypes.string,
|
|
227
|
-
/*
|
|
228
|
-
Path used for "native link"
|
|
229
|
-
*/
|
|
230
|
-
nativePath: PropTypes.string,
|
|
231
106
|
onAppSwitch: PropTypes.func,
|
|
232
107
|
app: PropTypes.shape({
|
|
233
108
|
// Slug of the app : drive / banks ...
|
|
234
|
-
slug: PropTypes.string.isRequired
|
|
235
|
-
// Information about mobile native app
|
|
236
|
-
mobile: PropTypes.shape({
|
|
237
|
-
schema: PropTypes.string,
|
|
238
|
-
id_playstore: PropTypes.string,
|
|
239
|
-
id_appstore: PropTypes.string
|
|
240
|
-
})
|
|
109
|
+
slug: PropTypes.string.isRequired
|
|
241
110
|
}).isRequired
|
|
242
111
|
}
|
|
243
112
|
|
|
244
113
|
export default withClient(AppLinker)
|
|
245
|
-
export {
|
|
246
|
-
NATIVE_APP_INFOS,
|
|
247
|
-
getUniversalLinkDomain,
|
|
248
|
-
generateWebLink,
|
|
249
|
-
generateUniversalLink
|
|
250
|
-
}
|
|
114
|
+
export { getUniversalLinkDomain, generateWebLink, generateUniversalLink }
|
|
@@ -2,17 +2,8 @@ import { render } from '@testing-library/react'
|
|
|
2
2
|
import userEvent from '@testing-library/user-event'
|
|
3
3
|
import React from 'react'
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
isMobileApp,
|
|
7
|
-
isMobile,
|
|
8
|
-
openDeeplinkOrRedirect,
|
|
9
|
-
startApp,
|
|
10
|
-
isAndroid,
|
|
11
|
-
checkApp
|
|
12
|
-
} from 'cozy-device-helper'
|
|
13
|
-
|
|
14
5
|
import AppLinker from '.'
|
|
15
|
-
|
|
6
|
+
|
|
16
7
|
jest.useFakeTimers()
|
|
17
8
|
|
|
18
9
|
const setup = ({ app, onAppSwitch }) => {
|
|
@@ -32,21 +23,6 @@ const setup = ({ app, onAppSwitch }) => {
|
|
|
32
23
|
}
|
|
33
24
|
}
|
|
34
25
|
|
|
35
|
-
jest.mock('./native', () => ({
|
|
36
|
-
...jest.requireActual('./native'),
|
|
37
|
-
generateUniversalLink: jest.fn()
|
|
38
|
-
}))
|
|
39
|
-
|
|
40
|
-
jest.mock('cozy-device-helper', () => ({
|
|
41
|
-
...jest.requireActual('cozy-device-helper'),
|
|
42
|
-
isMobileApp: jest.fn(),
|
|
43
|
-
isMobile: jest.fn(),
|
|
44
|
-
openDeeplinkOrRedirect: jest.fn(),
|
|
45
|
-
startApp: jest.fn().mockResolvedValue(),
|
|
46
|
-
isAndroid: jest.fn(),
|
|
47
|
-
checkApp: jest.fn()
|
|
48
|
-
}))
|
|
49
|
-
|
|
50
26
|
const app = {
|
|
51
27
|
slug: 'drive',
|
|
52
28
|
name: 'Drive'
|
|
@@ -56,16 +32,12 @@ describe('app icon', () => {
|
|
|
56
32
|
let spyConsoleError, appSwitchMock
|
|
57
33
|
|
|
58
34
|
beforeEach(() => {
|
|
59
|
-
isMobileApp.mockReturnValue(false)
|
|
60
35
|
spyConsoleError = jest.spyOn(console, 'error')
|
|
61
36
|
spyConsoleError.mockImplementation(message => {
|
|
62
37
|
if (message.lastIndexOf('Warning: Failed prop type:') === 0) {
|
|
63
38
|
throw new Error(message)
|
|
64
39
|
}
|
|
65
40
|
})
|
|
66
|
-
isMobileApp.mockReturnValue(false)
|
|
67
|
-
isMobile.mockReturnValue(false)
|
|
68
|
-
isAndroid.mockReturnValue(false)
|
|
69
41
|
appSwitchMock = jest.fn()
|
|
70
42
|
})
|
|
71
43
|
|
|
@@ -80,74 +52,9 @@ describe('app icon', () => {
|
|
|
80
52
|
})
|
|
81
53
|
|
|
82
54
|
it('should work for web -> web', async () => {
|
|
83
|
-
isMobileApp.mockReturnValue(false)
|
|
84
55
|
const { container, user } = setup({ app, onAppSwitch: appSwitchMock })
|
|
85
56
|
const link = container.querySelector('a')
|
|
86
57
|
await user.click(link)
|
|
87
58
|
expect(appSwitchMock).not.toHaveBeenCalled()
|
|
88
|
-
expect(startApp).not.toHaveBeenCalled()
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('should work for native -> native', async () => {
|
|
92
|
-
isMobileApp.mockReturnValue(true)
|
|
93
|
-
checkApp.mockResolvedValue(true)
|
|
94
|
-
const { container, user } = setup({ app, onAppSwitch: appSwitchMock })
|
|
95
|
-
const link = container.querySelector('a')
|
|
96
|
-
await user.click(link)
|
|
97
|
-
expect(startApp).toHaveBeenCalledWith({
|
|
98
|
-
appId: 'io.cozy.drive.mobile',
|
|
99
|
-
name: 'Cozy Drive',
|
|
100
|
-
uri: 'cozydrive://'
|
|
101
|
-
})
|
|
102
|
-
expect(appSwitchMock).toHaveBeenCalled()
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('should work for web -> native for Android (custom schema) ', async () => {
|
|
106
|
-
isMobile.mockReturnValue(true)
|
|
107
|
-
isAndroid.mockResolvedValue(true)
|
|
108
|
-
const { container, user } = setup({ app, onAppSwitch: appSwitchMock })
|
|
109
|
-
const link = container.querySelector('a')
|
|
110
|
-
await user.click(link)
|
|
111
|
-
expect(openDeeplinkOrRedirect).toHaveBeenCalledWith(
|
|
112
|
-
'cozydrive://',
|
|
113
|
-
expect.any(Function)
|
|
114
|
-
)
|
|
115
|
-
expect(appSwitchMock).toHaveBeenCalled()
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('should work for web -> native for iOS (universal link)', async () => {
|
|
119
|
-
isMobile.mockReturnValue(true)
|
|
120
|
-
const { container, user } = setup({ app, onAppSwitch: appSwitchMock })
|
|
121
|
-
const link = container.querySelector('a')
|
|
122
|
-
await user.click(link)
|
|
123
|
-
|
|
124
|
-
expect(generateUniversalLink).toHaveBeenCalled()
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('should work for native -> web', async () => {
|
|
128
|
-
isMobileApp.mockReturnValue(true)
|
|
129
|
-
const { container, user } = setup({ app, onAppSwitch: appSwitchMock })
|
|
130
|
-
const link = container.querySelector('a')
|
|
131
|
-
await user.click(link)
|
|
132
|
-
expect(appSwitchMock).toHaveBeenCalled()
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('should not crash if no href', () => {
|
|
136
|
-
isMobileApp.mockReturnValue(true)
|
|
137
|
-
spyConsoleError.mockImplementation(() => {})
|
|
138
|
-
const { container } = render(
|
|
139
|
-
<AppLinker onAppSwitch={appSwitchMock} app={app}>
|
|
140
|
-
{({ onClick, href, name }) => {
|
|
141
|
-
return (
|
|
142
|
-
<div>
|
|
143
|
-
<a href={href} onClick={onClick}>
|
|
144
|
-
Open {name}
|
|
145
|
-
</a>
|
|
146
|
-
</div>
|
|
147
|
-
)
|
|
148
|
-
}}
|
|
149
|
-
</AppLinker>
|
|
150
|
-
)
|
|
151
|
-
expect(container).toMatchSnapshot()
|
|
152
59
|
})
|
|
153
60
|
})
|
|
@@ -1,15 +1 @@
|
|
|
1
|
-
import { isAndroidApp } from 'cozy-device-helper'
|
|
2
|
-
|
|
3
|
-
export const NATIVE_APP_INFOS = {
|
|
4
|
-
drive: {
|
|
5
|
-
appId: 'io.cozy.drive.mobile',
|
|
6
|
-
uri: 'cozydrive://',
|
|
7
|
-
name: 'Cozy Drive'
|
|
8
|
-
},
|
|
9
|
-
banks: {
|
|
10
|
-
appId: isAndroidApp() ? 'io.cozy.banks.mobile' : 'io.cozy.banks',
|
|
11
|
-
uri: 'cozybanks://',
|
|
12
|
-
name: 'Cozy Banks'
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
1
|
export const UNIVERSAL_LINK_URL = 'https://links.mycozy.cloud'
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
|
|
2
1
|
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
|
|
3
2
|
import _createClass from "@babel/runtime/helpers/createClass";
|
|
4
3
|
import _assertThisInitialized from "@babel/runtime/helpers/assertThisInitialized";
|
|
@@ -11,8 +10,6 @@ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (O
|
|
|
11
10
|
|
|
12
11
|
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
|
|
13
12
|
|
|
14
|
-
import _regeneratorRuntime from "@babel/runtime/regenerator";
|
|
15
|
-
|
|
16
13
|
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
|
|
17
14
|
|
|
18
15
|
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
|
|
@@ -20,20 +17,10 @@ function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Re
|
|
|
20
17
|
import PropTypes from 'prop-types';
|
|
21
18
|
import React from 'react';
|
|
22
19
|
import { withClient } from 'cozy-client';
|
|
23
|
-
import {
|
|
20
|
+
import { isFlagshipApp } from 'cozy-device-helper';
|
|
24
21
|
import { WebviewContext } from 'cozy-intent';
|
|
25
22
|
import logger from 'cozy-logger';
|
|
26
|
-
import expiringMemoize from "cozy-ui/transpiled/react/AppLinker/expiringMemoize";
|
|
27
23
|
import { generateUniversalLink, generateWebLink, getUniversalLinkDomain } from "cozy-ui/transpiled/react/AppLinker/native";
|
|
28
|
-
import { NATIVE_APP_INFOS } from "cozy-ui/transpiled/react/AppLinker/native.config";
|
|
29
|
-
var expirationDelay = 10 * 1000;
|
|
30
|
-
var memoizedCheckApp = expiringMemoize(function (appInfo) {
|
|
31
|
-
return checkApp(appInfo).catch(function () {
|
|
32
|
-
return false;
|
|
33
|
-
});
|
|
34
|
-
}, expirationDelay, function (appInfo) {
|
|
35
|
-
return appInfo.appId;
|
|
36
|
-
});
|
|
37
24
|
export var AppLinker = /*#__PURE__*/function (_React$Component) {
|
|
38
25
|
_inherits(AppLinker, _React$Component);
|
|
39
26
|
|
|
@@ -47,8 +34,7 @@ export var AppLinker = /*#__PURE__*/function (_React$Component) {
|
|
|
47
34
|
_this = _super.call(this, props);
|
|
48
35
|
|
|
49
36
|
_defineProperty(_assertThisInitialized(_this), "state", {
|
|
50
|
-
|
|
51
|
-
isFetchingAppInfo: false
|
|
37
|
+
imgRef: null
|
|
52
38
|
});
|
|
53
39
|
|
|
54
40
|
_defineProperty(_assertThisInitialized(_this), "setImgRef", function (img) {
|
|
@@ -59,105 +45,36 @@ export var AppLinker = /*#__PURE__*/function (_React$Component) {
|
|
|
59
45
|
});
|
|
60
46
|
});
|
|
61
47
|
|
|
62
|
-
_this.imgRef = null;
|
|
63
48
|
return _this;
|
|
64
49
|
}
|
|
65
50
|
|
|
66
51
|
_createClass(AppLinker, [{
|
|
67
|
-
key: "componentDidMount",
|
|
68
|
-
value: function componentDidMount() {
|
|
69
|
-
if (isMobileApp()) {
|
|
70
|
-
this.checkAppAvailability();
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}, {
|
|
74
|
-
key: "checkAppAvailability",
|
|
75
|
-
value: function () {
|
|
76
|
-
var _checkAppAvailability = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
|
|
77
|
-
var slug, appInfo, nativeAppIsAvailable;
|
|
78
|
-
return _regeneratorRuntime.wrap(function _callee$(_context) {
|
|
79
|
-
while (1) {
|
|
80
|
-
switch (_context.prev = _context.next) {
|
|
81
|
-
case 0:
|
|
82
|
-
slug = AppLinker.getSlug(this.props);
|
|
83
|
-
appInfo = NATIVE_APP_INFOS[slug];
|
|
84
|
-
|
|
85
|
-
if (!appInfo) {
|
|
86
|
-
_context.next = 9;
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
_context.t0 = Boolean;
|
|
91
|
-
_context.next = 6;
|
|
92
|
-
return memoizedCheckApp(appInfo);
|
|
93
|
-
|
|
94
|
-
case 6:
|
|
95
|
-
_context.t1 = _context.sent;
|
|
96
|
-
nativeAppIsAvailable = (0, _context.t0)(_context.t1);
|
|
97
|
-
this.setState({
|
|
98
|
-
nativeAppIsAvailable: nativeAppIsAvailable
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
case 9:
|
|
102
|
-
case "end":
|
|
103
|
-
return _context.stop();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}, _callee, this);
|
|
107
|
-
}));
|
|
108
|
-
|
|
109
|
-
function checkAppAvailability() {
|
|
110
|
-
return _checkAppAvailability.apply(this, arguments);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return checkAppAvailability;
|
|
114
|
-
}()
|
|
115
|
-
}, {
|
|
116
52
|
key: "render",
|
|
117
53
|
value: function render() {
|
|
118
54
|
var children = this.props.children;
|
|
119
|
-
AppLinker.deprecateSlug(this.props);
|
|
120
|
-
var slug = AppLinker.getSlug(this.props);
|
|
121
|
-
var nativeAppIsAvailable = this.state.nativeAppIsAvailable;
|
|
122
|
-
var appInfo = NATIVE_APP_INFOS[slug];
|
|
123
55
|
|
|
124
|
-
var _AppLinker$getOnClick = AppLinker.getOnClickHref(this.props,
|
|
56
|
+
var _AppLinker$getOnClick = AppLinker.getOnClickHref(this.props, this.context, this.state.imgRef),
|
|
125
57
|
href = _AppLinker$getOnClick.href,
|
|
126
58
|
onClick = _AppLinker$getOnClick.onClick;
|
|
127
59
|
|
|
128
|
-
return children(
|
|
60
|
+
return children({
|
|
129
61
|
iconRef: this.setImgRef,
|
|
130
62
|
onClick: onClick,
|
|
131
63
|
href: href
|
|
132
|
-
})
|
|
64
|
+
});
|
|
133
65
|
}
|
|
134
66
|
}], [{
|
|
135
67
|
key: "getSlug",
|
|
136
68
|
value: function getSlug(props) {
|
|
137
|
-
|
|
138
|
-
return props.app.slug;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return props.slug;
|
|
142
|
-
}
|
|
143
|
-
}, {
|
|
144
|
-
key: "deprecateSlug",
|
|
145
|
-
value: function deprecateSlug(props) {
|
|
146
|
-
if (props.slug) {
|
|
147
|
-
console.warn("AppLinker's 'slug' prop is deprecated, please use 'app.slug' instead");
|
|
148
|
-
}
|
|
69
|
+
return props.app.slug;
|
|
149
70
|
}
|
|
150
71
|
}, {
|
|
151
72
|
key: "getOnClickHref",
|
|
152
|
-
value: function getOnClickHref(props,
|
|
73
|
+
value: function getOnClickHref(props, context, imgRef) {
|
|
153
74
|
var app = props.app,
|
|
154
|
-
client = props.client
|
|
155
|
-
nativePath = props.nativePath;
|
|
156
|
-
var slug = AppLinker.getSlug(props);
|
|
75
|
+
client = props.client;
|
|
157
76
|
var href = props.href;
|
|
158
77
|
var onClick = null;
|
|
159
|
-
var usingNativeApp = isMobileApp();
|
|
160
|
-
var appInfo = NATIVE_APP_INFOS[slug];
|
|
161
78
|
|
|
162
79
|
if (isFlagshipApp()) {
|
|
163
80
|
var _ref = client ? client.getInstanceOptions() : undefined,
|
|
@@ -175,64 +92,11 @@ export var AppLinker = /*#__PURE__*/function (_React$Component) {
|
|
|
175
92
|
}
|
|
176
93
|
}
|
|
177
94
|
|
|
178
|
-
if (usingNativeApp) {
|
|
179
|
-
if (nativeAppIsAvailable) {
|
|
180
|
-
// If we are on the native app and the other native app is available,
|
|
181
|
-
// we open the native app
|
|
182
|
-
onClick = AppLinker.openNativeFromNative.bind(this, props);
|
|
183
|
-
href = '#';
|
|
184
|
-
} else {
|
|
185
|
-
// If we are on a native app, but the other native app is not available
|
|
186
|
-
// we open the web link, this is done by the href prop. We still
|
|
187
|
-
// have to call the prop callback
|
|
188
|
-
onClick = AppLinker.openWeb.bind(this, props);
|
|
189
|
-
}
|
|
190
|
-
} else if (isMobile() && appInfo) {
|
|
191
|
-
// If we are on the "mobile web version", we try to open the native app
|
|
192
|
-
// if it exists with an universal links. If it fails, we redirect to the web
|
|
193
|
-
// version of the requested app
|
|
194
|
-
// Only on iOS ATM
|
|
195
|
-
if (isAndroid()) {
|
|
196
|
-
onClick = AppLinker.openNativeFromWeb.bind(this, props);
|
|
197
|
-
} else {
|
|
198
|
-
// Since generateUniversalLink can rise an error, let's catch it to not crash
|
|
199
|
-
// all the page.
|
|
200
|
-
try {
|
|
201
|
-
href = generateUniversalLink({
|
|
202
|
-
slug: slug,
|
|
203
|
-
nativePath: nativePath,
|
|
204
|
-
fallbackUrl: href
|
|
205
|
-
});
|
|
206
|
-
} catch (err) {
|
|
207
|
-
console.error(err);
|
|
208
|
-
href = '#';
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
95
|
return {
|
|
214
96
|
href: href,
|
|
215
97
|
onClick: onClick
|
|
216
98
|
};
|
|
217
99
|
}
|
|
218
|
-
}, {
|
|
219
|
-
key: "openNativeFromWeb",
|
|
220
|
-
value: function openNativeFromWeb(props, ev) {
|
|
221
|
-
var href = props.href,
|
|
222
|
-
nativePath = props.nativePath,
|
|
223
|
-
onAppSwitch = props.onAppSwitch;
|
|
224
|
-
var slug = AppLinker.getSlug(props);
|
|
225
|
-
var appInfo = NATIVE_APP_INFOS[slug];
|
|
226
|
-
|
|
227
|
-
if (ev) {
|
|
228
|
-
ev.preventDefault();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
AppLinker.onAppSwitch(onAppSwitch);
|
|
232
|
-
openDeeplinkOrRedirect(appInfo.uri + (nativePath === '/' ? '' : nativePath), function () {
|
|
233
|
-
window.location.href = href;
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
100
|
}, {
|
|
237
101
|
key: "onAppSwitch",
|
|
238
102
|
value: function onAppSwitch(onAppSwitchFn) {
|
|
@@ -240,22 +104,6 @@ export var AppLinker = /*#__PURE__*/function (_React$Component) {
|
|
|
240
104
|
onAppSwitchFn();
|
|
241
105
|
}
|
|
242
106
|
}
|
|
243
|
-
}, {
|
|
244
|
-
key: "openNativeFromNative",
|
|
245
|
-
value: function openNativeFromNative(props, ev) {
|
|
246
|
-
var onAppSwitch = props.onAppSwitch;
|
|
247
|
-
var slug = AppLinker.getSlug(props);
|
|
248
|
-
|
|
249
|
-
if (ev) {
|
|
250
|
-
ev.preventDefault();
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
var appInfo = NATIVE_APP_INFOS[slug];
|
|
254
|
-
AppLinker.onAppSwitch(onAppSwitch);
|
|
255
|
-
startApp(appInfo).catch(function (err) {
|
|
256
|
-
console.error('AppLinker: Could not open native app', err);
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
107
|
}, {
|
|
260
108
|
key: "openWeb",
|
|
261
109
|
value: function openWeb(props) {
|
|
@@ -268,34 +116,17 @@ export var AppLinker = /*#__PURE__*/function (_React$Component) {
|
|
|
268
116
|
|
|
269
117
|
_defineProperty(AppLinker, "contextType", WebviewContext);
|
|
270
118
|
|
|
271
|
-
AppLinker.defaultProps = {
|
|
272
|
-
nativePath: '/'
|
|
273
|
-
};
|
|
274
119
|
AppLinker.propTypes = {
|
|
275
|
-
/** DEPRECATED: please use app.slug prop */
|
|
276
|
-
slug: PropTypes.string,
|
|
277
|
-
|
|
278
120
|
/*
|
|
279
121
|
Full web url : Used by default on desktop browser
|
|
280
122
|
Used as a fallback_uri on mobile web
|
|
281
123
|
*/
|
|
282
124
|
href: PropTypes.string,
|
|
283
|
-
|
|
284
|
-
/*
|
|
285
|
-
Path used for "native link"
|
|
286
|
-
*/
|
|
287
|
-
nativePath: PropTypes.string,
|
|
288
125
|
onAppSwitch: PropTypes.func,
|
|
289
126
|
app: PropTypes.shape({
|
|
290
127
|
// Slug of the app : drive / banks ...
|
|
291
|
-
slug: PropTypes.string.isRequired
|
|
292
|
-
// Information about mobile native app
|
|
293
|
-
mobile: PropTypes.shape({
|
|
294
|
-
schema: PropTypes.string,
|
|
295
|
-
id_playstore: PropTypes.string,
|
|
296
|
-
id_appstore: PropTypes.string
|
|
297
|
-
})
|
|
128
|
+
slug: PropTypes.string.isRequired
|
|
298
129
|
}).isRequired
|
|
299
130
|
};
|
|
300
131
|
export default withClient(AppLinker);
|
|
301
|
-
export {
|
|
132
|
+
export { getUniversalLinkDomain, generateWebLink, generateUniversalLink };
|
|
@@ -1,14 +1 @@
|
|
|
1
|
-
import { isAndroidApp } from 'cozy-device-helper';
|
|
2
|
-
export var NATIVE_APP_INFOS = {
|
|
3
|
-
drive: {
|
|
4
|
-
appId: 'io.cozy.drive.mobile',
|
|
5
|
-
uri: 'cozydrive://',
|
|
6
|
-
name: 'Cozy Drive'
|
|
7
|
-
},
|
|
8
|
-
banks: {
|
|
9
|
-
appId: isAndroidApp() ? 'io.cozy.banks.mobile' : 'io.cozy.banks',
|
|
10
|
-
uri: 'cozybanks://',
|
|
11
|
-
name: 'Cozy Banks'
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
1
|
export var UNIVERSAL_LINK_URL = 'https://links.mycozy.cloud';
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
-
|
|
3
|
-
exports[`app icon should not crash if no href 1`] = `
|
|
4
|
-
<div>
|
|
5
|
-
<div>
|
|
6
|
-
<a>
|
|
7
|
-
Open
|
|
8
|
-
Cozy Drive
|
|
9
|
-
</a>
|
|
10
|
-
</div>
|
|
11
|
-
</div>
|
|
12
|
-
`;
|
|
13
|
-
|
|
14
|
-
exports[`app icon should render correctly 1`] = `
|
|
15
|
-
<div>
|
|
16
|
-
<div>
|
|
17
|
-
<a
|
|
18
|
-
href="https://fake.link"
|
|
19
|
-
>
|
|
20
|
-
Open
|
|
21
|
-
Cozy Drive
|
|
22
|
-
</a>
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
`;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export default function (fn, duration, keyFn) {
|
|
2
|
-
const memo = {}
|
|
3
|
-
return arg => {
|
|
4
|
-
const key = keyFn(arg)
|
|
5
|
-
const memoInfo = memo[key]
|
|
6
|
-
const uptodate =
|
|
7
|
-
memoInfo && memoInfo.result && memoInfo.date - Date.now() < duration
|
|
8
|
-
if (!uptodate) {
|
|
9
|
-
memo[key] = { result: fn(arg), date: Date.now() }
|
|
10
|
-
}
|
|
11
|
-
return memo[key].result
|
|
12
|
-
}
|
|
13
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Those tests are here to test previous AppLinker implementation base on
|
|
3
|
-
* `slug` prop.
|
|
4
|
-
* Now that AppLinkers implements `app` prop and uses `app.slug`, the old
|
|
5
|
-
* `slug` prop is deprecated
|
|
6
|
-
* Those tests should be kept until `slug` prop is completely removed
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { render } from '@testing-library/react'
|
|
10
|
-
import userEvent from '@testing-library/user-event'
|
|
11
|
-
import React from 'react'
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
isMobileApp,
|
|
15
|
-
isMobile,
|
|
16
|
-
openDeeplinkOrRedirect,
|
|
17
|
-
startApp,
|
|
18
|
-
isAndroid,
|
|
19
|
-
checkApp
|
|
20
|
-
} from 'cozy-device-helper'
|
|
21
|
-
|
|
22
|
-
import AppLinker from '.'
|
|
23
|
-
import { generateUniversalLink } from './native'
|
|
24
|
-
jest.useFakeTimers()
|
|
25
|
-
|
|
26
|
-
const setup = ({ app, onAppSwitch }) => {
|
|
27
|
-
return {
|
|
28
|
-
user: userEvent.setup({ delay: null }),
|
|
29
|
-
...render(
|
|
30
|
-
<AppLinker
|
|
31
|
-
onAppSwitch={onAppSwitch}
|
|
32
|
-
slug={app.slug}
|
|
33
|
-
href="https://fake.link"
|
|
34
|
-
>
|
|
35
|
-
{({ onClick, href, name }) => (
|
|
36
|
-
<div>
|
|
37
|
-
<a href={href} onClick={onClick}>
|
|
38
|
-
Open {name}
|
|
39
|
-
</a>
|
|
40
|
-
</div>
|
|
41
|
-
)}
|
|
42
|
-
</AppLinker>
|
|
43
|
-
)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
jest.mock('./native', () => ({
|
|
48
|
-
...jest.requireActual('./native'),
|
|
49
|
-
generateUniversalLink: jest.fn()
|
|
50
|
-
}))
|
|
51
|
-
|
|
52
|
-
jest.mock('cozy-device-helper', () => ({
|
|
53
|
-
...jest.requireActual('cozy-device-helper'),
|
|
54
|
-
isMobileApp: jest.fn(),
|
|
55
|
-
isMobile: jest.fn(),
|
|
56
|
-
openDeeplinkOrRedirect: jest.fn(),
|
|
57
|
-
startApp: jest.fn().mockResolvedValue(),
|
|
58
|
-
isAndroid: jest.fn(),
|
|
59
|
-
checkApp: jest.fn()
|
|
60
|
-
}))
|
|
61
|
-
|
|
62
|
-
const app = {
|
|
63
|
-
slug: 'drive',
|
|
64
|
-
name: 'Drive'
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
describe('app icon', () => {
|
|
68
|
-
let spyConsoleError, spyConsoleWarn, appSwitchMock
|
|
69
|
-
|
|
70
|
-
beforeEach(() => {
|
|
71
|
-
isMobileApp.mockReturnValue(false)
|
|
72
|
-
spyConsoleError = jest.spyOn(console, 'error')
|
|
73
|
-
spyConsoleError.mockImplementation(() => {})
|
|
74
|
-
spyConsoleWarn = jest.spyOn(console, 'warn')
|
|
75
|
-
spyConsoleWarn.mockImplementation(() => {})
|
|
76
|
-
isMobileApp.mockReturnValue(false)
|
|
77
|
-
isMobile.mockReturnValue(false)
|
|
78
|
-
isAndroid.mockReturnValue(false)
|
|
79
|
-
appSwitchMock = jest.fn()
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
afterEach(() => {
|
|
83
|
-
spyConsoleError.mockRestore()
|
|
84
|
-
spyConsoleWarn.mockRestore()
|
|
85
|
-
jest.restoreAllMocks()
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('should render correctly', () => {
|
|
89
|
-
const { container } = setup({ app })
|
|
90
|
-
expect(container).toMatchSnapshot()
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('should work for web -> web', async () => {
|
|
94
|
-
isMobileApp.mockReturnValue(false)
|
|
95
|
-
const { container, user } = setup({ app, onAppSwitch: appSwitchMock })
|
|
96
|
-
const link = container.querySelector('a')
|
|
97
|
-
await user.click(link)
|
|
98
|
-
expect(appSwitchMock).not.toHaveBeenCalled()
|
|
99
|
-
expect(startApp).not.toHaveBeenCalled()
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('should work for native -> native', async () => {
|
|
103
|
-
isMobileApp.mockReturnValue(true)
|
|
104
|
-
checkApp.mockResolvedValue(true)
|
|
105
|
-
const { container, user } = setup({ app, onAppSwitch: appSwitchMock })
|
|
106
|
-
const link = container.querySelector('a')
|
|
107
|
-
await user.click(link)
|
|
108
|
-
|
|
109
|
-
expect(startApp).toHaveBeenCalledWith({
|
|
110
|
-
appId: 'io.cozy.drive.mobile',
|
|
111
|
-
name: 'Cozy Drive',
|
|
112
|
-
uri: 'cozydrive://'
|
|
113
|
-
})
|
|
114
|
-
expect(appSwitchMock).toHaveBeenCalled()
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('should work for web -> native for Android (custom schema)', async () => {
|
|
118
|
-
isMobile.mockReturnValue(true)
|
|
119
|
-
isAndroid.mockResolvedValue(true)
|
|
120
|
-
const { container, user } = setup({ app, onAppSwitch: appSwitchMock })
|
|
121
|
-
const link = container.querySelector('a')
|
|
122
|
-
await user.click(link)
|
|
123
|
-
expect(openDeeplinkOrRedirect).toHaveBeenCalledWith(
|
|
124
|
-
'cozydrive://',
|
|
125
|
-
expect.any(Function)
|
|
126
|
-
)
|
|
127
|
-
expect(appSwitchMock).toHaveBeenCalled()
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('should work for web -> native for iOS (universal link)', async () => {
|
|
131
|
-
isMobile.mockReturnValue(true)
|
|
132
|
-
const { container, user } = setup({ app, onAppSwitch: appSwitchMock })
|
|
133
|
-
const link = container.querySelector('a')
|
|
134
|
-
await user.click(link)
|
|
135
|
-
|
|
136
|
-
expect(generateUniversalLink).toHaveBeenCalled()
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
it('should work for native -> web', async () => {
|
|
140
|
-
isMobileApp.mockReturnValue(true)
|
|
141
|
-
const { container, user } = setup({ app, onAppSwitch: appSwitchMock })
|
|
142
|
-
const link = container.querySelector('a')
|
|
143
|
-
await user.click(link)
|
|
144
|
-
expect(appSwitchMock).toHaveBeenCalled()
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('should not crash if no href', () => {
|
|
148
|
-
isMobileApp.mockReturnValue(true)
|
|
149
|
-
const { container } = render(
|
|
150
|
-
<AppLinker onAppSwitch={appSwitchMock} slug={app.slug}>
|
|
151
|
-
{({ onClick, href, name }) => (
|
|
152
|
-
<div>
|
|
153
|
-
<a href={href} onClick={onClick}>
|
|
154
|
-
Open {name}
|
|
155
|
-
</a>
|
|
156
|
-
</div>
|
|
157
|
-
)}
|
|
158
|
-
</AppLinker>
|
|
159
|
-
)
|
|
160
|
-
expect(container).toMatchSnapshot()
|
|
161
|
-
})
|
|
162
|
-
})
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export default function (fn, duration, keyFn) {
|
|
2
|
-
var memo = {};
|
|
3
|
-
return function (arg) {
|
|
4
|
-
var key = keyFn(arg);
|
|
5
|
-
var memoInfo = memo[key];
|
|
6
|
-
var uptodate = memoInfo && memoInfo.result && memoInfo.date - Date.now() < duration;
|
|
7
|
-
|
|
8
|
-
if (!uptodate) {
|
|
9
|
-
memo[key] = {
|
|
10
|
-
result: fn(arg),
|
|
11
|
-
date: Date.now()
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return memo[key].result;
|
|
16
|
-
};
|
|
17
|
-
}
|