Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(stack-client): Get Icon Url uses preloaded url when oAuth not needed #1134

Merged
merged 1 commit into from
Mar 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appData now mandatory to be able to be executed simply => https://github.com/cozy/cozy-ui/pull/2064/files

} 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