Skip to content

Commit

Permalink
refactor: adjust Brave activation UI flow
Browse files Browse the repository at this point in the history
- remove custom HTML UI (Brave 1.23.22+ provides own UI)
- use bafkqaaa as the trigger
- cleanup bafkqaaa, switch to Preferences/Welcome page
- single codebase, works in both old and new Brave

Closes #982
  • Loading branch information
lidel committed Mar 10, 2021
1 parent e457302 commit 07ed8b1
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 32 deletions.
3 changes: 2 additions & 1 deletion add-on/src/landing-pages/welcome/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const { renderTranslatedLinks, renderTranslatedSpans } = require('../../utils/i1

// Brave detection
const { brave } = require('../../../src/lib/ipfs-client/brave')
const { optionsPage } = require('../../../src/lib/constants')

// Assets
const libp2pLogo = '../../../images/libp2p.svg'
Expand Down Expand Up @@ -99,7 +100,7 @@ const renderInstallSteps = (i18n, isIpfsOnline) => {
</svg>
`

const optionsUrl = browser.extension.getURL('dist/options/options.html')
const optionsUrl = browser.extension.getURL(optionsPage)
return html`
<div class="w-80 mt0 flex flex-column transition-all ${stateUnknown && 'state-unknown'}">
<div class="mb4 flex flex-column justify-center items-center">
Expand Down
6 changes: 6 additions & 0 deletions add-on/src/lib/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict'
/* eslint-env browser, webextensions */

exports.welcomePage = '/dist/landing-pages/welcome/index.html'
exports.optionsPage = '/dist/options/options.html'
exports.tickMs = 250 // no CPU spike, but still responsive enough
106 changes: 82 additions & 24 deletions add-on/src/lib/ipfs-client/brave.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ log.error = debug('ipfs-companion:client:brave:error')
const external = require('./external')
const toUri = require('multiaddr-to-uri')
const pWaitFor = require('p-wait-for')
const { welcomePage, optionsPage, tickMs } = require('../constants')

// increased interval to decrease impact of IPFS service process spawns
const waitFor = (f, t) => pWaitFor(f, { interval: 250, timeout: t || Infinity })
const waitFor = (f, t) => pWaitFor(f, { interval: tickMs, timeout: t || Infinity })

exports.init = async function (browser, opts) {
log('ensuring Brave Settings are correct')
Expand All @@ -37,13 +38,17 @@ exports.destroy = async function (browser) {
// ---------------- Brave-specifics -------------------

// ipfs:// URI that will be used for triggering the "Enable IPFS" dropbar in Brave
const braveIpfsUriTrigger = 'ipfs://bafkreigxbf77se2an2u6hmg2kxxbhmenetc7dzvkd3rl4m2orlobjvqcqq'
// Here we use inlined empty byte array, which resolves instantly and does not
// introduce any delay in UI.
const braveIpfsUriTrigger = 'ipfs://bafkqaaa/'
const braveGatewayUrlTrigger = 'https://bafkqaaa.ipfs.dweb.link/'

// Settings screen in Brave where user can manage IPFS support
const braveSettingsPage = 'brave://settings/extensions'
// TODO: replace with brave://settings/ipfs after https://github.com/brave/brave-browser/issues/13655 lands in Brave Stable

// Diagnostic page for manually starting/stopping Brave's node
// const braveIpfsDiagnosticPage = 'brave://ipfs'
// const braveIpfsDiagnosticPage = 'brave://ipfs' // TODO: https://github.com/brave/brave-browser/issues/14500

// ipfsNodeType for this backend
exports.braveNodeType = 'external:brave'
Expand Down Expand Up @@ -169,27 +174,12 @@ function addrs2url (addr) {
}

async function initBraveSettings (browser, brave) {
let showState = () => {}
let tabId
let method = await brave.getResolveMethodType()
log(`brave.resolveMethodType is '${method}'`)

if (method === 'ask') {
// Trigger the dropbar with "Enable IPFS" button by opening ipfs:// URI in a new tab.
// The trigger is a HTML page with some text to make onboarding easier.
tabId = (await browser.tabs.create({ url: braveIpfsUriTrigger })).id

// Reuse the tab for state updates (or create a new one if user closes it)
// Caveat: we inject JS as we can't use tab.update during the init of local gateway
// because Brave will try to use it and fail as it is not ready yet :-))
showState = async (s) => {
try {
await browser.tabs.executeScript(tabId, { code: `window.location.hash = '#${s}';` })
} catch (e) { // noop, just log, don't break if user closed the tab etc
log.error('error while showState', e)
}
}
showState('ask')
await browser.tabs.create({ url: braveIpfsUriTrigger })

// IPFS Companion is unable to change Brave settings,
// all we can do is to poll chrome.ipfs.* and detect when user made a decision
Expand All @@ -202,28 +192,96 @@ async function initBraveSettings (browser, brave) {

if (method === 'local') {
log('waiting while Brave downloads IPFS executable..')
showState('download')
await waitFor(() => brave.getExecutableAvailable())

log('waiting while Brave creates repo and config via ipfs init..')
await showState('init')
await waitFor(async () => typeof (await brave.getConfig()) !== 'undefined')
}
}

if (method !== 'local') {
await showState('ask')
// close tab with temporary trigger URI
await closeIpfsTab(browser, braveIpfsUriTrigger)
await closeIpfsTab(browser, braveGatewayUrlTrigger)
// open settings
await browser.tabs.create({ url: braveSettingsPage })
throw new Error('"Method to resolve IPFS resources" in Brave settings should be "Local node"')
}

// ensure local node is started
log('waiting while brave.launch() starts ipfs daemon..')
await showState('start')
await waitFor(() => brave.launch())
log('brave.launch() finished')
await showState('done')

// ensure Companion uses the endpoint provided by Brave
await exports.useBraveEndpoint(browser)

// async UI cleanup, after other stuff
setTimeout(() => activationUiCleanup(browser), tickMs)
}

// close tab in a way that works with ipfs://
async function closeIpfsTab (browser, tabUrl) {
// fun bug: querying for { url: 'ipfs://..' } does not work,
// but if we query for { } ipfs:// tabs are returned just fine,
// so we do that and discard unwanted ones ¯\_(ツ)_/¯
// TODO: fix chrome.tabs.query when we care about upstreaming things to Chromium
for (const tab of await browser.tabs.query({})) {
if (tab.url === tabUrl) {
await browser.tabs.remove(tab.id)
}
}
}

// Various tedious tasks that need to happen for nice UX:
// - wait for gateway to be up (indicates node finished booting)
// - close ephemeral activation tab
// - re-activate entry point (options or welcome page)
// - ignore unexpected failures (user could do something weird, close tab before time etc)
async function activationUiCleanup (browser) {
try {
// after useBraveEndpoint we can start polling for gateway to become online
const { customGatewayUrl: braveGwUrl } = await browser.storage.local.get('customGatewayUrl')
// wait 1m for gateway to be online (bafkqaaa)
await waitFor(async () => {
try {
return await fetch(`${braveGwUrl}/ipfs/bafkqaaa`).then(response => response.ok)
} catch (_) {
return false
}
})
log('[activation ui cleanup] Brave gateway is up, cleaning up')

const welcomePageUrl = browser.extension.getURL(welcomePage)
const optionsPageUrl = browser.extension.getURL(optionsPage)
// we are unable to query ipfs:// directly due to reasons mentioned in 'closeIpfsTab'
// so we make quick pass over all tabs and check welcome and options while at it.
for (const tab of await browser.tabs.query({})) {
try {
// close tab with temporary trigger
if (tab.url === braveIpfsUriTrigger || tab.url === braveGatewayUrlTrigger) {
await browser.tabs.remove(tab.id)
}
// switch to welcome page if present (onboarding via fresh install)
if (tab.url === welcomePageUrl) {
await browser.tabs.reload(tab.id)
await browser.tabs.update(tab.id, { active: true })
}
// switch to options page if present (onboarding via Preferences)
if (tab.url === optionsPageUrl) {
await browser.tabs.update(tab.id, { active: true })
}
} catch (e) {
log.error('[activation ui cleanup] unexpected error, but safe to ignore', e)
continue
}
}
log('[activation ui cleanup] done')

// (if ok or not, close temporary tab and switch to welcome page or open it if not existing
// if ((await browser.tabs.get(tabId)).url.startsWith(braveIpfsUriTrigger)) {
} catch (e) {
// most likely tab is gone (closed by user, etc)
log.error('[activation ui cleanup] failed to cleanup ephemeral UI tab', e)
}
}
4 changes: 2 additions & 2 deletions add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const LRU = require('lru-cache')
const all = require('it-all')
const { optionDefaults, storeMissingOptions, migrateOptions, guiURLString, safeURL } = require('./options')
const { initState, offlinePeerCount } = require('./state')
const { createIpfsPathValidator, sameGateway } = require('./ipfs-path')
const { createIpfsPathValidator, sameGateway, safeHostname } = require('./ipfs-path')
const createDnslinkResolver = require('./dnslink')
const { createRequestModifier } = require('./ipfs-request')
const { initIpfsClient, destroyIpfsClient } = require('./ipfs-client')
Expand Down Expand Up @@ -301,7 +301,7 @@ module.exports = async function init () {
}
}
info.currentDnslinkFqdn = dnslinkResolver.findDNSLinkHostname(url)
info.currentFqdn = info.currentDnslinkFqdn || new URL(url).hostname
info.currentFqdn = info.currentDnslinkFqdn || safeHostname(url)
info.currentTabIntegrationsOptOut = !state.activeIntegrations(info.currentFqdn)
info.isRedirectContext = info.currentFqdn && ipfsPathValidator.isRedirectPageActionsContext(url)
}
Expand Down
12 changes: 12 additions & 0 deletions add-on/src/lib/ipfs-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@ function sameGateway (url, gwUrl) {
}
exports.sameGateway = sameGateway

const safeHostname = (url) => {
// In case vendor-specific thing like brave://settings/extensions
// cause errors, we don't throw, just return null
try {
return new URL(url).hostname
} catch (e) {
console.error(`[ipfs-companion] safeHostname(url) error for url='${url}'`, e)
}
return null
}
exports.safeHostname = safeHostname

function createIpfsPathValidator (getState, getIpfs, dnslinkResolver) {
const ipfsPathValidator = {
// Test if URL is a Public IPFS resource
Expand Down
4 changes: 2 additions & 2 deletions add-on/src/lib/on-installed.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
const browser = require('webextension-polyfill')
const { version } = browser.runtime.getManifest()

exports.welcomePage = '/dist/landing-pages/welcome/index.html'
const { welcomePage } = require('./constants')
exports.updatePage = 'https://github.com/ipfs-shipyard/ipfs-companion/releases/tag/v'

exports.onInstalled = async (details) => {
Expand All @@ -23,7 +23,7 @@ exports.runPendingOnInstallTasks = async () => {
case 'onFirstInstall':
await useNativeNodeIfFeasible(browser)
return browser.tabs.create({
url: exports.welcomePage
url: welcomePage
})
case 'onVersionUpdate':
if (!displayReleaseNotes) return
Expand Down
4 changes: 2 additions & 2 deletions add-on/src/popup/browser-action/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const browser = require('webextension-polyfill')
const isIPFS = require('is-ipfs')
const all = require('it-all')
const { trimHashAndSearch, ipfsContentPath } = require('../../lib/ipfs-path')
const { welcomePage } = require('../../lib/on-installed')
const { welcomePage, optionsPage } = require('../../lib/constants')
const { contextMenuViewOnGateway, contextMenuCopyAddressAtPublicGw, contextMenuCopyPermalink, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress, contextMenuCopyCidAddress } = require('../../lib/context-menus')

// The store contains and mutates the state for the app
Expand Down Expand Up @@ -188,7 +188,7 @@ module.exports = (state, emitter) => {
.catch((err) => {
console.error('runtime.openOptionsPage() failed, opening options page in tab instead.', err)
// brave: fallback to opening options page as a tab.
browser.tabs.create({ url: browser.extension.getURL('dist/options/options.html') })
browser.tabs.create({ url: browser.extension.getURL(optionsPage) })
})
})

Expand Down
14 changes: 13 additions & 1 deletion test/functional/lib/ipfs-path.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { stub } = require('sinon')
const { describe, it, beforeEach, afterEach } = require('mocha')
const { expect } = require('chai')
const { URL } = require('url')
const { ipfsUri, ipfsContentPath, createIpfsPathValidator, sameGateway } = require('../../../add-on/src/lib/ipfs-path')
const { ipfsUri, ipfsContentPath, createIpfsPathValidator, sameGateway, safeHostname } = require('../../../add-on/src/lib/ipfs-path')
const { initState } = require('../../../add-on/src/lib/state')
const createDnslinkResolver = require('../../../add-on/src/lib/dnslink')
const { optionDefaults } = require('../../../add-on/src/lib/options')
Expand Down Expand Up @@ -109,6 +109,18 @@ describe('ipfs-path.js', function () {
})
})

describe('safeHostname', function () {
it('should return URL.hostname on http URL', function () {
const url = 'https://example.com:8080/path/file.txt'
expect(safeHostname(url)).to.equal('example.com')
})
it('should return null on error', function () {
const url = ''
expect(safeHostname(url)).to.equal(null)
})
})


// TODO: move to some lib?
describe('ipfsUri', function () {
it('should detect /ipfs/ path in URL from a public gateway', function () {
Expand Down

0 comments on commit 07ed8b1

Please sign in to comment.