diff --git a/app/assets/images/logo-forem-app.svg b/app/assets/images/logo-forem-app.svg new file mode 100644 index 000000000..2d00f3890 --- /dev/null +++ b/app/assets/images/logo-forem-app.svg @@ -0,0 +1,4 @@ + diff --git a/app/assets/javascripts/initializePage.js b/app/assets/javascripts/initializePage.js index ce7384343..17d8e4a2c 100644 --- a/app/assets/javascripts/initializePage.js +++ b/app/assets/javascripts/initializePage.js @@ -4,7 +4,7 @@ initializeBaseTracking, initializeCommentsPage, initializeArticleDate, initializeArticleReactions, initNotifications, initializeCommentDate, initializeCommentDropdown, initializeSettings, - initializeCommentPreview, + initializeCommentPreview, initializeRuntimeBanner, initializeTimeFixer, initializeDashboardSort, initializePWAFunctionality, initializeEllipsisMenu, initializeArchivedPostFilter, initializeCreditsPage, initializeUserProfilePage, initializeProfileInfoToggle, initializePodcastPlayback, @@ -17,6 +17,7 @@ function callInitializers() { initializeBaseTracking(); + initializeRuntimeBanner(); initializePaymentPointers(); initializeCommentsPage(); initializeArticleDate(); diff --git a/app/assets/javascripts/initializers/initializeRuntimeBanner.js b/app/assets/javascripts/initializers/initializeRuntimeBanner.js new file mode 100644 index 000000000..e62610de1 --- /dev/null +++ b/app/assets/javascripts/initializers/initializeRuntimeBanner.js @@ -0,0 +1,85 @@ +/* global Runtime */ + +/** + * Callback that dismisses the Runtime Banner + */ +function handleDismissRuntimeBanner() { + const runtimeBanner = document.querySelector('.runtime-banner'); + if (runtimeBanner) { + runtimeBanner.remove(); + } +} + +/** + * This function redirects the browser to custom schemes, i.e. deep link into + * mobile apps. A separate function is needed in this case in order to test + * the feature using Cypress. More details can be found here: + * https://medium.com/cypress-io-thailand/understand-stub-spy-apis-that-will-make-your-test-better-than-ever-on-cypress-io-797cb9eb205a#b6ad + * + * @param {string} targetURI - The target custom scheme URI + */ +function launchCustomSchemeDeepLink(targetURI) { + window.location.href = targetURI; +} + +/** + * Function that run on every page load (including InstantClick redirects) and + * is in charge of the setup that runs the following features: + * - addEventListener to dismiss the banner when users tap on X button + * - When the user lands in "/r/mobile" + * - Timeout that presents the fallback page if couldn't seamless deep link + * - Setup the correct fallback button links based on platform (iOS/Android) + */ +function initializeRuntimeBanner() { + // This will provide the dismiss functionality for the Runtime Banner + const bannerDismiss = document.querySelector('.runtime-banner__dismiss'); + if (bannerDismiss) { + bannerDismiss.addEventListener('click', handleDismissRuntimeBanner); + } + + // If the "Install now"/"Try again" buttons exist in the DOM it means we are + // trying to deep link into the mobile app after being redirected by the + // Runtime Banner itself (the browser is currently in `/r/mobile`) + const installNowButton = document.getElementById('link-to-mobile-install'); + const retryButton = document.getElementById('link-to-mobile-install-retry'); + if (!installNowButton || !retryButton) { + return; + } + // Since we were redirected to `/r/mobile` by the Banner itself we can safely + // dismiss it by re-using the handleDismissRuntimeBanner function + handleDismissRuntimeBanner(); + + // The target path comes in via GET request query params + const urlParams = new URLSearchParams(window.location.search); + const targetPath = urlParams.get('deep_link'); + + // Constants - they will become dynamic (configurable by creators) in upcoming releases + const FOREM_IOS_SCHEME = 'com.forem.app'; + const FOREM_APP_STORE_URL = + 'https://apps.apple.com/us/app/dev-community/id1439094790'; + const FOREM_GOOGLE_PLAY_URL = + 'https://play.google.com/store/apps/details?id=to.dev.dev_android'; + + if (Runtime.currentOS() === 'iOS') { + // The install now must target Apple's AppStore + installNowButton.href = FOREM_APP_STORE_URL; + + // We try to deep link directly by launching a custom scheme and populate + // the retry button in case the user will need it + const targetLink = `${FOREM_IOS_SCHEME}://${window.location.host}${targetPath}`; + retryButton.href = targetLink; + launchCustomSchemeDeepLink(targetLink); + } else if (Runtime.currentOS() === 'Android') { + const targetIntent = + 'intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;end'; + retryButton.href = targetIntent; + installNowButton.href = FOREM_GOOGLE_PLAY_URL; + + // Android support will not be available yet. Links to `/r/mobile` won't be + // visible in Android browsers. However, as a fallback (safety) measure to + // avoid having Android users land in this page we redirect them back to the + // home page. This to avoids users landing in an unsupported (not working + // for them) page. + window.location.href = '/'; + } +} diff --git a/app/assets/stylesheets/runtime-banner.scss b/app/assets/stylesheets/runtime-banner.scss index ce0487951..0e25113cb 100644 --- a/app/assets/stylesheets/runtime-banner.scss +++ b/app/assets/stylesheets/runtime-banner.scss @@ -1,15 +1,40 @@ -.runtime-banner { - position: fixed; - left: 0; - right: 0; - bottom: calc(65px + env(safe-area-inset-bottom)); - z-index: var(--z-sticky); +@import 'variables'; +@import '_mixins'; - &__inner { - background-color: #0b0c0c; - color: #fff; - display: flex; - align-items: center; - justify-content: space-around; +.runtime-banner { + display: flex; + margin: 0 auto; + left: var(--su-4); + right: var(--su-4); + position: fixed; + bottom: calc(var(--su-9) + env(safe-area-inset-bottom)); + flex-flow: row nowrap; + align-items: center; + padding: var(--su-1); + max-width: calc(100% - var(--su-4) * 2); + z-index: var(--z-popover); + border-radius: var(--radius); + background: var(--snackbar-bg); + color: var(--snackbar-color); + font-size: var(--fs-s); + + :any-link { + color: inherit; + } +} + +.mobile-deep-link-banner { + margin: 30px; + text-align: center; + + .crayons-card { + max-width: 500px; + margin: 30px auto; + } +} + +#mobile-deep-link-spinner { + svg { + margin: 30px auto; } } diff --git a/app/controllers/deep_links_controller.rb b/app/controllers/deep_links_controller.rb new file mode 100644 index 000000000..efcc72c8b --- /dev/null +++ b/app/controllers/deep_links_controller.rb @@ -0,0 +1,23 @@ +class DeepLinksController < ApplicationController + def mobile; end + + # Apple Application Site Association + def aasa + # TODO: [@fdoxyz] Replace these hardcoded identifiers with configurations + # creators can use to customize their Forems - `/admin/consumer_apps` + supported_apps = ["R9SWHSQNV8.com.forem.app"] + supported_apps << "R9SWHSQNV8.to.dev.ios" if SiteConfig.dev_to? + render json: { + applinks: { + apps: supported_apps, + details: supported_apps.map { |app_id| { appID: app_id, paths: ["/*"] } } + }, + activitycontinuation: { + apps: supported_apps + }, + webcredentials: { + apps: supported_apps + } + } + end +end diff --git a/app/lib/url.rb b/app/lib/url.rb index af1f6f283..f8d21106c 100644 --- a/app/lib/url.rb +++ b/app/lib/url.rb @@ -67,6 +67,16 @@ module URL ActionController::Base.helpers.image_url(image_name, host: host) end + # Creates a deep link URL (for mobile) to a page in the current Forem and it + # relies on a UDL server to bounce back mobile users to the local `/r/mobile` + # fallback page. More details here: https://github.com/forem/udl-server + # + # @param path [String] the target path to deep link + def self.deep_link(path) + target_path = CGI.escape(url("/r/mobile?deep_link=#{path}")) + "https://forem-udl-server.herokuapp.com/?r=#{target_path}" + end + def self.organization(organization) url(organization.slug) end diff --git a/app/views/deep_links/mobile.html.erb b/app/views/deep_links/mobile.html.erb new file mode 100644 index 000000000..1f8cce0ec --- /dev/null +++ b/app/views/deep_links/mobile.html.erb @@ -0,0 +1,48 @@ +
Opening the mobile app...
+