Skip to content

Commit

Permalink
fix(stack-client): Get Icon Url uses preloaded url when oAuth not needed
Browse files Browse the repository at this point in the history
  • Loading branch information
trollepierre committed Mar 1, 2022
1 parent e2ccdec commit 34611df
Show file tree
Hide file tree
Showing 4 changed files with 334 additions and 175 deletions.
39 changes: 31 additions & 8 deletions docs/api/cozy-stack-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ See <a href="https://docs.cozy.io/en/cozy-stack/sharing-design/#description-of-a
<dt><a href="#getAccessToken">getAccessToken()</a> ⇒ <code>string</code></dt>
<dd><p>Get the app token string</p>
</dd>
<dt><a href="#getIconURL">getIconURL(stackClient, opts)</a> ⇒ <code>Promise.&lt;string&gt;</code> | <code>string</code></dt>
<dd><p>Get Icon URL using blob mechanism if OAuth connected
or using preloaded url when blob not needed</p>
</dd>
<dt><a href="#garbageCollect">garbageCollect()</a></dt>
<dd><p>Delete outdated results from cache</p>
</dd>
Expand Down Expand Up @@ -264,7 +268,7 @@ Main API against the `cozy-stack` server.

* [CozyStackClient](#CozyStackClient)
* [.collection(doctype)](#CozyStackClient+collection)[<code>DocumentCollection</code>](#DocumentCollection)
* [.fetch(method, path, body, opts)](#CozyStackClient+fetch) ⇒ <code>object</code>
* [.fetch(method, path, [body], [opts])](#CozyStackClient+fetch) ⇒ <code>object</code>
* [.checkForRevocation()](#CozyStackClient+checkForRevocation)
* [.refreshToken()](#CozyStackClient+refreshToken) ⇒ <code>Promise</code>
* [.fetchJSON(method, path, body, options)](#CozyStackClient+fetchJSON) ⇒ <code>object</code>
Expand All @@ -284,7 +288,7 @@ Creates a [DocumentCollection](#DocumentCollection) instance.

<a name="CozyStackClient+fetch"></a>

### cozyStackClient.fetch(method, path, body, opts) ⇒ <code>object</code>
### cozyStackClient.fetch(method, path, [body], [opts]) ⇒ <code>object</code>
Fetches an endpoint in an authorized way.

**Kind**: instance method of [<code>CozyStackClient</code>](#CozyStackClient)
Expand All @@ -293,12 +297,12 @@ Fetches an endpoint in an authorized way.
- <code>FetchError</code>


| Param | Type | Description |
| --- | --- | --- |
| method | <code>string</code> | The HTTP method. |
| path | <code>string</code> | The URI. |
| body | <code>object</code> | The payload. |
| opts | <code>object</code> | Options for fetch |
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| method | <code>string</code> | | The HTTP method. |
| path | <code>string</code> | | The URI. |
| [body] | <code>object</code> | | The payload. |
| [opts] | <code>object</code> | <code>{}</code> | Options for fetch |

<a name="CozyStackClient+checkForRevocation"></a>

Expand Down Expand Up @@ -1820,6 +1824,25 @@ Get the app token string
**Kind**: global function
**Returns**: <code>string</code> - token
**See**: CozyStackClient.getAccessToken
<a name="getIconURL"></a>

## getIconURL(stackClient, opts) ⇒ <code>Promise.&lt;string&gt;</code> \| <code>string</code>
Get Icon URL using blob mechanism if OAuth connected
or using preloaded url when blob not needed

**Kind**: global function
**Returns**: <code>Promise.&lt;string&gt;</code> \| <code>string</code> - DOMString containing URL source or a URL representing the Blob or ErrorReturned

| Param | Type | Default | Description |
| --- | --- | --- | --- |
| stackClient | [<code>CozyStackClient</code>](#CozyStackClient) | | CozyStackClient |
| stackClient.oauthOptions | <code>object</code> | | oauthOptions used to detect fetching mechanism |
| opts | <code>object</code> | | Options |
| opts.type | <code>string</code> | | Options type |
| opts.slug | <code>string</code> | | Options slug |
| opts.appData | <code>object</code> | | Apps data - io.cozy.apps |
| [opts.priority] | <code>string</code> | <code>&quot;&#x27;stack&#x27;&quot;</code> | Options priority |

<a name="garbageCollect"></a>

## garbageCollect()
Expand Down
4 changes: 2 additions & 2 deletions packages/cozy-stack-client/src/CozyStackClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ class CozyStackClient {
*
* @param {string} method The HTTP method.
* @param {string} path The URI.
* @param {object} body The payload.
* @param {object} opts Options for fetch
* @param {object} [body] The payload.
* @param {object} [opts={}] Options for fetch
* @returns {object}
* @throws {FetchError}
*/
Expand Down
166 changes: 129 additions & 37 deletions packages/cozy-stack-client/src/getIconURL.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,49 @@
import memoize, { ErrorReturned } from './memoize'

/**
* Get Icon source Url
*
* @param {object} app - Apps data - io.cozy.apps
* @param {string|undefined} domain - Host to use in the origin (e.g. cozy.tools)
* @param {string} protocol - Url protocol (e.g. http / https)
* @returns {string} Source Url of icon
* @private
* @throws {Error} When cannot fetch or get icon source
*/
const loadIcon = async (app, domain, protocol) => {
if (!domain) throw new Error('Cannot fetch icon: missing domain')
const source = _getAppIconURL(app, domain, protocol)
if (!source) {
throw new Error(`Cannot get icon source for app ${app.name}`)
}
return source
}

/**
* Get App Icon URL
*
* @param {object|string} app - Apps data - io.cozy.apps
* @param {string|undefined} domain - Host to use in the origin (e.g. cozy.tools)
* @param {string} protocol - Url protocol (e.g. http / https)
* @private
* @returns {string|null} App Icon URL
*/
const _getAppIconURL = (app, domain, protocol) => {
const path = (app && app.links && app.links.icon) || _getRegistryIconPath(app)
return path ? `${protocol}//${domain}${path}` : null
}

/**
* Get Registry Icon Path
*
* @param {object|string} app - Apps data - io.cozy.apps
* @returns {string|undefined} Registry icon path
* @private
*/
const _getRegistryIconPath = app =>
app?.latest_version?.version &&
`/registry/${app.slug}/${app.latest_version.version}/icon`

const mimeTypes = {
gif: 'image/gif',
ico: 'image/vnd.microsoft.icon',
Expand All @@ -8,6 +53,16 @@ const mimeTypes = {
svg: 'image/svg+xml'
}

/**
* Get icon extension
*
* @param {object} app io.cozy.apps or io.cozy.konnectors document
* @param {string} app.icon - App Icon
* @param {string} app.name - App Name
* @returns {string} icon extension
* @private
* @throws {Error} When problem while detecting icon mime type
*/
const getIconExtensionFromApp = app => {
if (!app.icon) {
throw new Error(
Expand Down Expand Up @@ -58,52 +113,89 @@ const fetchAppOrKonnectorViaRegistry = (stackClient, type, slug) =>
.fetchJSON('GET', `/registry/${slug}`)
.then(x => x.latest_version.manifest)

const _getIconURL = async (stackClient, opts) => {
/**
* Get Icon URL using blob mechanism if OAuth connected
* or using preloaded url when blob not needed
*
* @param {CozyStackClient} stackClient - CozyStackClient
* @param {object} stackClient.oauthOptions - oauthOptions used to detect fetching mechanism
* @param {object} opts - Options
* @param {string} opts.type - Options type
* @param {string} opts.slug - Options slug
* @param {object} opts.appData - Apps data - io.cozy.apps
* @param {string} [opts.priority='stack'] - Options priority
* @returns {Promise<string>|string} DOMString containing URL source or a URL representing the Blob .
* @private
* @throws {Error} while fetching icon, or unknown image extension
*/
export const _getIconURL = async (stackClient, opts) => {
const { type, slug, appData, priority = 'stack' } = opts
const iconDataFetchers = [
() => stackClient.fetch('GET', `/${type}s/${slug}/icon`),
() => stackClient.fetch('GET', `/registry/${slug}/icon`)
]
if (priority === 'registry') {
iconDataFetchers.reverse()
}
const resp = await fallbacks(iconDataFetchers, resp => {
if (!resp.ok) {
throw new Error(`Error while fetching icon ${resp.statusText}`)
}
})
let icon = await resp.blob()
let app
if (!icon.type) {
// iOS10 does not set correctly mime type for images, so we assume
// that an empty mime type could mean that the app is running on iOS10.
// For regular images like jpeg, png or gif it still works well in the
// Safari browser but not for SVG.
// So let's set a mime type manually. We cannot always set it to
// image/svg+xml and must guess the mime type based on the icon attribute
// from app/manifest
// See https://stackoverflow.com/questions/38318411/uiwebview-on-ios-10-beta-not-loading-any-svg-images
const appDataFetchers = [
() => fetchAppOrKonnector(stackClient, type, slug),
() => fetchAppOrKonnectorViaRegistry(stackClient, type, slug)
if (stackClient.oauthOptions) {
const iconDataFetchers = [
() => stackClient.fetch('GET', `/${type}s/${slug}/icon`),
() => stackClient.fetch('GET', `/registry/${slug}/icon`)
]
if (priority === 'registry') {
appDataFetchers.reverse()
iconDataFetchers.reverse()
}
const resp = await fallbacks(iconDataFetchers, resp => {
if (!resp.ok) {
throw new Error(`Error while fetching icon ${resp.statusText}`)
}
})
let icon = await resp.blob()
let app
if (!icon.type) {
// iOS10 does not set correctly mime type for images, so we assume
// that an empty mime type could mean that the app is running on iOS10.
// For regular images like jpeg, png or gif it still works well in the
// Safari browser but not for SVG.
// So let's set a mime type manually. We cannot always set it to
// image/svg+xml and must guess the mime type based on the icon attribute
// from app/manifest
// See https://stackoverflow.com/questions/38318411/uiwebview-on-ios-10-beta-not-loading-any-svg-images
const appDataFetchers = [
() => fetchAppOrKonnector(stackClient, type, slug),
() => fetchAppOrKonnectorViaRegistry(stackClient, type, slug)
]
if (priority === 'registry') {
appDataFetchers.reverse()
}
app = appData || (await fallbacks(appDataFetchers)) || {}
const ext = getIconExtensionFromApp(app)
if (!mimeTypes[ext]) {
throw new Error(`Unknown image extension "${ext}" for app ${app.name}`)
}
icon = new Blob([icon], { type: mimeTypes[ext] })
}
app = appData || (await fallbacks(appDataFetchers)) || {}
const ext = getIconExtensionFromApp(app)
if (!mimeTypes[ext]) {
throw new Error(`Unknown image extension "${ext}" for app ${app.name}`)
return URL.createObjectURL(icon)
} else {
try {
const { host: domain, protocol } = new URL(stackClient.uri)
return loadIcon(appData, domain, protocol)
} catch (error) {
throw new Error(
`Cannot fetch icon: invalid stackClient.uri: ${error.message}`
)
}
icon = new Blob([icon], { type: mimeTypes[ext] })
}
return URL.createObjectURL(icon)
}

/**
* Get Icon URL using blob mechanism if OAuth connected
* or using preloaded url when blob not needed
*
* @param {CozyStackClient} stackClient - CozyStackClient
* @param {object} stackClient.oauthOptions - oauthOptions used to detect fetching mechanism
* @param {object} opts - Options
* @param {string} opts.type - Options type
* @param {string} opts.slug - Options slug
* @param {object} opts.appData - Apps data - io.cozy.apps
* @param {string} [opts.priority='stack'] - Options priority
* @returns {Promise<string>|string} DOMString containing URL source or a URL representing the Blob or ErrorReturned
*/
const getIconURL = function() {
return _getIconURL.apply(this, arguments).catch(e => {
return new ErrorReturned()
})
return _getIconURL.apply(this, arguments).catch(() => new ErrorReturned())
}

export default memoize(getIconURL, {
Expand Down
Loading

0 comments on commit 34611df

Please sign in to comment.