{"id":1380,"date":"2026-06-08T17:00:00","date_gmt":"2026-06-08T22:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1380"},"modified":"2026-03-07T03:35:11","modified_gmt":"2026-03-07T08:35:11","slug":"smart-banners-for-pwas","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/smart-banners-for-pwas\/","title":{"rendered":"Smart Banners for Progressive Web Apps"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Progressive Web Apps create an interesting challenge for smart banners. Traditional smart banners promote a native app install. But if you also have a PWA, which do you promote? The native app for deeper platform integration? The PWA for a lightweight install? Both? Neither (if the user is already using the PWA)?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The answer depends on what you ship: PWA only, native app only, or both. Each scenario requires a different banner strategy. This guide covers how to handle smart banners in each case. For the general smart banners setup, see the <a href=\"https:\/\/tolinku.com\/blog\/smart-app-banners-complete-guide\/\">smart banners guide<\/a>. For PWA and App Links interaction, see <a href=\"https:\/\/tolinku.com\/blog\/android-app-links-pwa\/\">App Links for PWAs<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><img decoding=\"async\" src=\"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/screenshot-banner-form-preview-1772822964529.png\" alt=\"Tolinku smart banner live preview in a mobile device frame\">\n<em>The live banner preview showing how the banner appears on a mobile device.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Scenario 1: Native App Only (No PWA)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the standard smart banner use case. You have a native iOS and Android app, and your website is a traditional web experience. The smart banner promotes the native app:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">const bannerConfig = {\n  ios: { appId: &quot;id123456789&quot;, storeUrl: &quot;https:\/\/apps.apple.com\/app\/id123456789&quot; },\n  android: { packageName: &quot;com.example.app&quot;, storeUrl: &quot;https:\/\/play.google.com\/store\/apps\/details?id=com.example.app&quot; },\n  message: &quot;Get the app for a better experience&quot;,\n  cta: &quot;Install&quot;,\n};\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">No PWA complications. The banner always promotes the native app.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Scenario 2: PWA Only (No Native App)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you only have a PWA and no native app, the smart banner promotes the PWA install (&quot;Add to Home Screen&quot;):<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Using the Web App Install Prompt<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Browsers provide a built-in install prompt for PWAs via the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Window\/beforeinstallprompt_event\" rel=\"nofollow noopener\" target=\"_blank\">beforeinstallprompt<\/a> event. You can trigger this from your smart banner:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">let deferredPrompt;\n\nwindow.addEventListener(&#39;beforeinstallprompt&#39;, (e) =&gt; {\n  \/\/ Prevent the default browser install prompt\n  e.preventDefault();\n  deferredPrompt = e;\n\n  \/\/ Show your custom smart banner\n  showPWAInstallBanner();\n});\n\nfunction showPWAInstallBanner() {\n  const banner = createBanner({\n    message: &quot;Install our app for offline access&quot;,\n    cta: &quot;Add to Home Screen&quot;,\n    onCtaClick: async () =&gt; {\n      if (deferredPrompt) {\n        deferredPrompt.prompt();\n        const result = await deferredPrompt.userChoice;\n        if (result.outcome === &#39;accepted&#39;) {\n          analytics.track(&#39;pwa_installed&#39;);\n        }\n        deferredPrompt = null;\n        hideBanner();\n      }\n    },\n  });\n  document.body.appendChild(banner);\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">PWA Banner Considerations<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Only show when installable.<\/strong> The <code>beforeinstallprompt<\/code> event fires only when the PWA meets the <a href=\"https:\/\/web.dev\/articles\/install-criteria\" rel=\"nofollow noopener\" target=\"_blank\">installability criteria<\/a>. Do not show a PWA install banner if the event has not fired.<\/li>\n<li><strong>Do not show to already-installed users.<\/strong> Check the display mode to detect if the PWA is already installed:<\/li>\n<\/ul>\n\n\n\n<pre><code class=\"language-javascript\">function isPWAInstalled() {\n  return window.matchMedia(&#39;(display-mode: standalone)&#39;).matches ||\n         window.matchMedia(&#39;(display-mode: fullscreen)&#39;).matches ||\n         window.navigator.standalone === true; \/\/ iOS Safari\n}\n<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>iOS Safari does not support <code>beforeinstallprompt<\/code>.<\/strong> On iOS, you cannot trigger the install prompt programmatically. Show instructions instead: &quot;Tap the share button, then &#39;Add to Home Screen&#39;.&quot;<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Scenario 3: Both Native App and PWA<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the complex case. You ship both a PWA and a native app. The banner needs to decide which to promote.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Decision Logic<\/h3>\n\n\n\n<pre><code class=\"language-javascript\">function chooseBannerType() {\n  const platform = detectPlatform();\n\n  \/\/ If the native app is installed, don&#39;t show any banner\n  if (isNativeAppInstalled()) return null;\n\n  \/\/ If the PWA is installed, don&#39;t show any banner\n  if (isPWAInstalled()) return null;\n\n  \/\/ Promote native app on platforms where it&#39;s available\n  if (platform === &#39;ios&#39; &amp;&amp; hasNativeIOSApp) return &#39;native&#39;;\n  if (platform === &#39;android&#39; &amp;&amp; hasNativeAndroidApp) return &#39;native&#39;;\n\n  \/\/ On desktop or unsupported platforms, promote PWA\n  if (platform === &#39;desktop&#39; &amp;&amp; hasPWA) return &#39;pwa&#39;;\n\n  \/\/ Fallback: promote native if available, otherwise PWA\n  return hasNativeApp ? &#39;native&#39; : &#39;pwa&#39;;\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">When to Promote Native Over PWA<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Promote the native app when:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The native app offers features the PWA cannot (push notifications on iOS, Bluetooth, NFC, widgets, Siri\/Google Assistant integration).<\/li>\n<li>The native app has significantly better performance for your use case (gaming, video editing, camera features).<\/li>\n<li>You want Play Store \/ App Store presence for discoverability.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">When to Promote PWA Over Native<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Promote the PWA when:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The user is on a desktop browser (no native mobile app context).<\/li>\n<li>The native app is not available on the user&#39;s platform.<\/li>\n<li>You want to reduce friction (PWA install is faster than app store download).<\/li>\n<li>The PWA provides an equivalent experience to the native app for this user&#39;s needs.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Showing Both Options<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In some cases, offer both:<\/p>\n\n\n\n<pre><code class=\"language-html\">&lt;div class=&quot;smart-banner smart-banner--dual&quot;&gt;\n  &lt;div class=&quot;smart-banner__content&quot;&gt;\n    &lt;img src=&quot;\/app-icon.png&quot; class=&quot;smart-banner__icon&quot; alt=&quot;App&quot; \/&gt;\n    &lt;p class=&quot;smart-banner__message&quot;&gt;Get our app&lt;\/p&gt;\n  &lt;\/div&gt;\n  &lt;div class=&quot;smart-banner__actions&quot;&gt;\n    &lt;a href=&quot;https:\/\/apps.apple.com\/...&quot; class=&quot;smart-banner__cta smart-banner__cta--native&quot;&gt;\n      App Store\n    &lt;\/a&gt;\n    &lt;button class=&quot;smart-banner__cta smart-banner__cta--pwa&quot; onclick=&quot;installPWA()&quot;&gt;\n      Add to Home Screen\n    &lt;\/button&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is less common because it creates decision paralysis. Usually, pick one and promote it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Detecting the User&#39;s Context<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Is the Native App Installed?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">There is no reliable cross-platform way to detect if a native app is installed from the web. Approaches include:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Custom scheme detection (limited):<\/strong><\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ This approach is unreliable and often blocked by browsers\nfunction checkNativeApp(customScheme) {\n  const start = Date.now();\n  window.location.href = `${customScheme}:\/\/check`;\n  setTimeout(() =&gt; {\n    if (Date.now() - start &lt; 2000) {\n      \/\/ App likely not installed (page didn&#39;t navigate)\n      showBanner();\n    }\n  }, 1500);\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Cookie-based detection:<\/strong>\nIf the native app sets a cookie via a shared web view or SDK, the web page can check for it:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function isNativeAppInstalled() {\n  return document.cookie.includes(&#39;native_app_installed=true&#39;);\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Tolinku detection:<\/strong>\n<a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku&#39;s SDK<\/a> provides app detection through the redirect chain. If the app is installed and handles the link, the user never reaches the web fallback.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Is the PWA Installed?<\/h3>\n\n\n\n<pre><code class=\"language-javascript\">function isPWAInstalled() {\n  \/\/ Check display mode\n  if (window.matchMedia(&#39;(display-mode: standalone)&#39;).matches) return true;\n\n  \/\/ iOS Safari standalone mode\n  if (window.navigator.standalone === true) return true;\n\n  \/\/ Check if the install prompt is available (if not, PWA might already be installed)\n  \/\/ Note: this is not conclusive; the prompt may not fire for other reasons\n\n  return false;\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Is This Running Inside a TWA?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If your native app is a Trusted Web Activity wrapping the PWA:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function isRunningInTWA() {\n  return document.referrer.includes(&#39;android-app:\/\/&#39;);\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Do not show any install banner inside a TWA. The user is already in the app.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Service Worker Considerations<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If your PWA has a service worker that caches pages, ensure the smart banner configuration is not cached aggressively:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ Service worker: do not cache banner configuration\nself.addEventListener(&#39;fetch&#39;, (event) =&gt; {\n  const url = new URL(event.request.url);\n\n  \/\/ Always fetch banner config from network\n  if (url.pathname.includes(&#39;\/api\/banner&#39;) || url.pathname.includes(&#39;\/banner-config&#39;)) {\n    event.respondWith(fetch(event.request));\n    return;\n  }\n\n  \/\/ Cache other resources normally\n  event.respondWith(\n    caches.match(event.request).then(cached =&gt; cached || fetch(event.request))\n  );\n});\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the banner config is cached, schedule changes and A\/B tests will not take effect until the cache expires.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku Smart Banners for PWAs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/smart-banners\">Tolinku&#39;s smart banners<\/a> support both native app and PWA promotion. The SDK detects the user&#39;s context (platform, installed apps, display mode) and shows the appropriate banner. Configure native app and PWA targets in the <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/smart-banners\/\">Tolinku dashboard<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the interaction between PWAs and App Links on Android, see <a href=\"https:\/\/tolinku.com\/blog\/android-app-links-pwa\/\">App Links for PWAs<\/a>. For the complete smart banners guide, see the <a href=\"https:\/\/tolinku.com\/blog\/smart-app-banners-complete-guide\/\">smart banners overview<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Use smart banners with Progressive Web Apps. Handle the PWA vs. native app decision, promote the right install path, and manage the overlap between web app install prompts and native app banners.<\/p>\n","protected":false},"author":2,"featured_media":1379,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Smart Banners for Progressive Web Apps","rank_math_description":"Use smart banners with Progressive Web Apps. Handle PWA vs native app decisions and promote the right install path.","rank_math_focus_keyword":"smart banners PWA","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-smart-banners-for-pwas.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-smart-banners-for-pwas.png","footnotes":""},"categories":[16],"tags":[20,110,69,343,326,40,33,41],"class_list":["post-1380","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-marketing","tag-deep-linking","tag-marketing","tag-mobile-development","tag-progressive-web-apps","tag-pwa","tag-smart-banners","tag-user-experience","tag-web-to-app"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1380","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=1380"}],"version-history":[{"count":3,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1380\/revisions"}],"predecessor-version":[{"id":2294,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1380\/revisions\/2294"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1379"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1380"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1380"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1380"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}