Skip to content

Commit

Permalink
feat: Universal link (#918)
Browse files Browse the repository at this point in the history
  • Loading branch information
Crash-- authored Apr 26, 2019
1 parent 6ade357 commit d5ecc53
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 32 deletions.
55 changes: 40 additions & 15 deletions react/AppLinker/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

import React from 'react'
import PropTypes from 'prop-types'

import {
checkApp,
startApp,
isMobileApp,
isMobile,
openDeeplinkOrRedirect
openDeeplinkOrRedirect,
isAndroid
} from 'cozy-device-helper'
import { NATIVE_APP_INFOS } from 'cozy-ui/transpiled/react/AppLinker/native'
import { generateUniversalLink, getUniversalLinkDomain } from './native'

import { NATIVE_APP_INFOS } from 'cozy-ui/transpiled/react/AppLinker/native.config'
import expiringMemoize from './expiringMemoize'

const expirationDelay = 10 * 1000
Expand All @@ -23,7 +24,8 @@ const memoizedCheckApp = expiringMemoize(

export class AppLinker extends React.Component {
state = {
nativeAppIsAvailable: null
nativeAppIsAvailable: null,
isFetchingAppInfo: false
}

constructor(props) {
Expand Down Expand Up @@ -67,28 +69,34 @@ export class AppLinker extends React.Component {
})
}

//Will be removed soon when Android universallink will be tested
openNativeFromWeb(ev) {
if (ev) {
ev.preventDefault()
}

const { href, slug } = this.props
const { href, slug, nativePath } = this.props
const appInfo = NATIVE_APP_INFOS[slug]

this.onAppSwitch()
openDeeplinkOrRedirect(appInfo.uri, function() {
window.location.href = href
})

openDeeplinkOrRedirect(
appInfo.uri + (nativePath === '/' ? '' : nativePath),
function() {
window.location.href = href
}
)
}

openWeb() {
this.onAppSwitch()
}

render() {
const { children, slug } = this.props
const { children, slug, nativePath } = this.props
const { nativeAppIsAvailable } = this.state
const appInfo = NATIVE_APP_INFOS[slug]

const appInfo = NATIVE_APP_INFOS[slug]
let href = this.props.href
let onClick = null
const usingNativeApp = isMobileApp()
Expand All @@ -107,19 +115,36 @@ export class AppLinker extends React.Component {
}
} else if (isMobile() && appInfo) {
// If we are on the "mobile web version", we try to open the native app
// if it exists. If it fails, we redirect to the web version of the
// requested app
onClick = this.openNativeFromWeb
// if it exists with an universal links. If it fails, we redirect to the web
// version of the requested app
// Only on iOS ATM
if (isAndroid()) {
onClick = this.openNativeFromWeb
} else {
href = generateUniversalLink({ slug, nativePath, fallbackUrl: href })
}
}

return children({ ...appInfo, onClick: onClick, href })
}
}

AppLinker.defaultProps = {
nativePath: '/'
}
AppLinker.propTypes = {
//Slug of the app : drive / banks ...
slug: PropTypes.string.isRequired,
href: PropTypes.string.isRequired
/*
Full web url : Used by default on desktop browser
Used as a fallback_uri on mobile web
*/
href: PropTypes.string.isRequired,
/*
Path used for "native link"
*/
nativePath: PropTypes.string
}

export default AppLinker
export { NATIVE_APP_INFOS }
export { NATIVE_APP_INFOS, getUniversalLinkDomain, generateUniversalLink }
26 changes: 22 additions & 4 deletions react/AppLinker/index.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import {
isMobileApp,
isMobile,
openDeeplinkOrRedirect,
startApp
startApp,
isAndroid
} from 'cozy-device-helper'

import AppLinker from './index'

import { generateUniversalLink } from './native'
jest.useFakeTimers()

const tMock = x => x
Expand All @@ -33,13 +34,18 @@ class AppItem extends React.Component {
)
}
}
jest.mock('./native', () => ({
...require.requireActual('./native'),
generateUniversalLink: jest.fn()
}))

jest.mock('cozy-device-helper', () => ({
...require.requireActual('cozy-device-helper'),
isMobileApp: jest.fn(),
isMobile: jest.fn(),
openDeeplinkOrRedirect: jest.fn(),
startApp: jest.fn().mockResolvedValue()
startApp: jest.fn().mockResolvedValue(),
isAndroid: jest.fn()
}))

const app = {
Expand All @@ -64,6 +70,7 @@ describe('app icon', () => {
)
isMobileApp.mockReturnValue(false)
isMobile.mockReturnValue(false)
isAndroid.mockReturnValue(false)
appSwitchMock = jest.fn()
})

Expand Down Expand Up @@ -95,8 +102,9 @@ describe('app icon', () => {
expect(appSwitchMock).toHaveBeenCalled()
})

it('should work for web -> native', () => {
it('should work for web -> native for Android (custom schema) ', () => {
isMobile.mockReturnValue(true)
isAndroid.mockResolvedValue(true)
const root = shallow(
<AppItem t={tMock} app={app} onAppSwitch={appSwitchMock} />
).dive()
Expand All @@ -108,6 +116,16 @@ describe('app icon', () => {
expect(appSwitchMock).toHaveBeenCalled()
})

it('should work for web -> native for iOS (universal link)', () => {
isMobile.mockReturnValue(true)
const root = shallow(
<AppItem t={tMock} app={app} onAppSwitch={appSwitchMock} />
).dive()
root.find('a').simulate('click', { preventDefault: () => {} })

expect(generateUniversalLink).toHaveBeenCalled()
})

it('should work for native -> web', () => {
isMobileApp.mockReturnValue(true)
const root = shallow(
Expand Down
15 changes: 15 additions & 0 deletions react/AppLinker/native.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { isAndroidApp } from 'cozy-device-helper'

export const NATIVE_APP_INFOS = {
drive: {
appId: 'io.cozy.drive.mobile',
uri: 'cozydrive://',
name: 'Cozy Drive'
},
banks: {
appId: isAndroidApp() ? 'io.cozy.banks.mobile' : 'io.cozy.banks',
uri: 'cozybanks://',
name: 'Cozy Banks'
}
}
export const UNIVERSAL_LINK_URL = 'https://links.mycozy.cloud'
32 changes: 19 additions & 13 deletions react/AppLinker/native.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { isAndroidApp } from 'cozy-device-helper'

export const NATIVE_APP_INFOS = {
drive: {
appId: 'io.cozy.drive.mobile',
uri: 'cozydrive://',
name: 'Cozy Drive'
},
banks: {
appId: isAndroidApp() ? 'io.cozy.banks.mobile' : 'io.cozy.banks',
uri: 'cozybanks://',
name: 'Cozy Banks'
}
import { UNIVERSAL_LINK_URL } from 'cozy-ui/transpiled/react/AppLinker/native.config.js'
export const getUniversalLinkDomain = () => {
return UNIVERSAL_LINK_URL
}
/*
slug string drive
path : /path/to/view
fallbackUrl: string : https://...mycozy.cloud
*/
export const generateUniversalLink = ({ slug, nativePath, fallbackUrl }) => {
if (!nativePath.startsWith('/')) nativePath = '/' + nativePath
return (
getUniversalLinkDomain() +
'/' +
slug +
nativePath +
'&fallback=' +
fallbackUrl
)
}
37 changes: 37 additions & 0 deletions react/AppLinker/native.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { generateUniversalLink, getUniversalLinkDomain } from './native'

describe('native functions', () => {
it('should generate the universalink', () => {
const universalLink = generateUniversalLink({
slug: 'drive',
nativePath: '/files/1',
fallbackUrl: 'https://drive.cozy.tools/files/1'
})
const endLink =
getUniversalLinkDomain() +
'/drive/files/1&fallback=https://drive.cozy.tools/files/1'
expect(universalLink).toEqual(endLink)
})

it('should generate the universalink for the home of an app', () => {
const universalLink = generateUniversalLink({
slug: 'drive',
nativePath: '/',
fallbackUrl: 'https://drive.cozy.tools/'
})
const endLink =
'https://links.mycozy.cloud/drive/&fallback=https://drive.cozy.tools/'
expect(universalLink).toEqual(endLink)
})

it('should generate the universalink for the home of an app even if no path', () => {
const universalLink = generateUniversalLink({
slug: 'drive',
nativePath: '',
fallbackUrl: 'https://drive.cozy.tools/'
})
const endLink =
getUniversalLinkDomain() + '/drive/&fallback=https://drive.cozy.tools/'
expect(universalLink).toEqual(endLink)
})
})

0 comments on commit d5ecc53

Please sign in to comment.