fix(ios): PERMANENT native safe-area + keyboard via Capacitor 7 + safe-area plugin

Device screenshots proved the WebView reports env(safe-area-inset-*)=0, so no web/CSS/JS trick can
move content off the Dynamic Island — it must be fixed natively. Upgrade the Capacitor shell to v7 and
add @capacitor-community/safe-area (populates env() with real per-device insets, edge-to-edge) and
@capacitor/keyboard (resize:native so the composer sits directly on the keyboard, like Teams).

- mobile: Capacitor 6 -> 7 for all plugins; add safe-area + keyboard; SafeArea/Keyboard config.
- codemagic: npm ci -> npm install (dependency set changed); drop stale Cap6 lockfile.
- web: remove the temporary StatusBar/34px hacks (env() now works); keep var(--sat)/var(--sab) padding.
- share/connect: back link now respects env(safe-area-inset-top/left).
- add native-style slide in/out (push/pop) animation when opening/closing a chat on mobile.
Build marker -> 2026-07-17-batch92.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 17:36:14 +05:30
parent a609bfd2e0
commit 2ce7ec72fb
8 changed files with 49 additions and 4982 deletions
+3 -1
View File
@@ -36,7 +36,9 @@ workflows:
- name: Install JS dependencies - name: Install JS dependencies
script: | script: |
cd mobile cd mobile
npm ci # npm install (not ci): the dependency set changed to Capacitor 7 + the safe-area/keyboard
# plugins, so we let npm resolve a fresh tree rather than require a pre-synced lockfile.
npm install
- name: Generate the iOS project (Capacitor) - name: Generate the iOS project (Capacitor)
script: | script: |
+8
View File
@@ -10,6 +10,14 @@
"plugins": { "plugins": {
"PushNotifications": { "PushNotifications": {
"presentationOptions": ["badge", "sound", "alert"] "presentationOptions": ["badge", "sound", "alert"]
},
"SafeArea": {
"detectViewportFitCoverChanges": true,
"initialViewportFitCover": true,
"offsetForKeyboardInsetBug": true
},
"Keyboard": {
"resize": "native"
} }
} }
} }
-4929
View File
File diff suppressed because it is too large Load Diff
+11 -9
View File
@@ -10,17 +10,19 @@
"ios": "cap open ios" "ios": "cap open ios"
}, },
"dependencies": { "dependencies": {
"@capacitor/android": "^6.1.0", "@capacitor-community/safe-area": "^7.0.0",
"@capacitor/app": "^6.0.0", "@capacitor/android": "^7.0.0",
"@capacitor/camera": "^6.0.0", "@capacitor/app": "^7.0.0",
"@capacitor/core": "^6.1.0", "@capacitor/camera": "^7.0.0",
"@capacitor/ios": "^6.1.0", "@capacitor/core": "^7.0.0",
"@capacitor/push-notifications": "^6.0.0", "@capacitor/ios": "^7.0.0",
"@capacitor/splash-screen": "^6.0.0", "@capacitor/keyboard": "^7.0.0",
"@capacitor/status-bar": "^6.0.0" "@capacitor/push-notifications": "^7.0.0",
"@capacitor/splash-screen": "^7.0.0",
"@capacitor/status-bar": "^7.0.0"
}, },
"devDependencies": { "devDependencies": {
"@capacitor/assets": "^3.0.5", "@capacitor/assets": "^3.0.5",
"@capacitor/cli": "^6.1.0" "@capacitor/cli": "^7.0.0"
} }
} }
+1 -9
View File
@@ -4,14 +4,6 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Biz Connect — Agent Console</title> <title>Biz Connect — Agent Console</title>
<script>
// Native shell: push content below the status bar/notch (the Capacitor WebView reports safe-area insets
// as 0, so the back button/header would otherwise slide under the Dynamic Island).
(function(){ try{ var C=window.Capacitor; if(!C||!C.isNativePlatform||!C.isNativePlatform()) return;
var p=(C.getPlatform&&C.getPlatform())||''; var r=document.documentElement; r.classList.add('native-app'); if(p) r.classList.add('native-'+p);
var SB=C.Plugins&&C.Plugins.StatusBar; if(SB){ try{ SB.setOverlaysWebView({overlay:false}); }catch(_){}; try{ SB.setStyle({style:'DARK'}); }catch(_){}; try{ SB.setBackgroundColor({color:'#ffffff'}); }catch(_){} }
}catch(_){} })();
</script>
<meta name="theme-color" content="#1F3B73"> <meta name="theme-color" content="#1F3B73">
<link rel="icon" href="/favicon.ico?v=3" sizes="any"> <link rel="icon" href="/favicon.ico?v=3" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png?v=3"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png?v=3">
@@ -71,7 +63,7 @@
.pwwrap input{padding-right:2.7rem;} .pwwrap input{padding-right:2.7rem;}
.eye{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);background:none;border:none;padding:.3rem;width:auto;color:var(--muted);display:inline-flex;align-items:center;cursor:pointer;margin:0;} .eye{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);background:none;border:none;padding:.3rem;width:auto;color:var(--muted);display:inline-flex;align-items:center;cursor:pointer;margin:0;}
.eye:hover{color:var(--blue);} .eye:hover{color:var(--blue);}
#homeLink{position:fixed;top:14px;left:16px;z-index:50;display:inline-flex;align-items:center;gap:6px;background:rgba(255,255,255,.92);color:#1F3B73;text-decoration:none;font-weight:600;font-size:.86rem;padding:.45rem .8rem;border-radius:10px;box-shadow:0 4px 12px rgba(0,0,0,.15);} #homeLink{position:fixed;top:calc(14px + env(safe-area-inset-top,0px));left:calc(16px + env(safe-area-inset-left,0px));z-index:50;display:inline-flex;align-items:center;gap:6px;background:rgba(255,255,255,.92);color:#1F3B73;text-decoration:none;font-weight:600;font-size:.86rem;padding:.45rem .8rem;border-radius:10px;box-shadow:0 4px 12px rgba(0,0,0,.15);}
.formerr{color:#b91c1c;font-weight:600;font-size:.88rem;margin-top:.9rem;min-height:1.1em;text-align:left;} .formerr{color:#b91c1c;font-weight:600;font-size:.88rem;margin-top:.9rem;min-height:1.1em;text-align:left;}
.formerr.show{display:flex;align-items:center;gap:.5rem;background:#fee2e2;border:1px solid #fca5a5;border-radius:9px;padding:.6rem .75rem;animation:errShake .35s;} .formerr.show{display:flex;align-items:center;gap:.5rem;background:#fee2e2;border:1px solid #fca5a5;border-radius:9px;padding:.6rem .75rem;animation:errShake .35s;}
.formerr.show::before{content:"⚠";font-size:1rem;} .formerr.show::before{content:"⚠";font-size:1rem;}
+25 -16
View File
@@ -23,11 +23,10 @@
bottom rail (the "gap"). overscroll-behavior:none pins the page; the rail's own height already bottom rail (the "gap"). overscroll-behavior:none pins the page; the rail's own height already
includes the home-indicator inset. */ includes the home-indicator inset. */
html,body{height:100%;overscroll-behavior:none;} html,body{height:100%;overscroll-behavior:none;}
/* Safe-area insets. The iOS Capacitor WebView reports env(safe-area-inset-*)=0, so on the native app /* Safe-area insets. The native app enables edge-to-edge via the @capacitor-community/safe-area plugin,
we (a) push content below the status bar/notch with StatusBar.setOverlaysWebView(false) — see the which populates env(safe-area-inset-*) with real values on every device (notch, Dynamic Island, home
native-chrome script — and (b) hard-code the bottom home-indicator inset here (env() can't give it). */ indicator). On the web these resolve to 0. All layout below pads with var(--sat)/var(--sab). */
:root{--sat:env(safe-area-inset-top,0px);--sab:env(safe-area-inset-bottom,0px);} :root{--sat:env(safe-area-inset-top,0px);--sab:env(safe-area-inset-bottom,0px);}
html.native-ios{--sab:34px;}
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;display:flex;flex-direction:column;height:100vh;height:100dvh;overflow:hidden;position:fixed;inset:0;width:100%;} body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;display:flex;flex-direction:column;height:100vh;height:100dvh;overflow:hidden;position:fixed;inset:0;width:100%;}
/* ---- Top bar ---- */ /* ---- Top bar ---- */
@@ -1069,6 +1068,13 @@
then only needs to clear the home indicator. */ then only needs to clear the home indicator. */
body.chat-open .rail{display:none;} body.chat-open .rail{display:none;}
body.chat-open .content .panel{bottom:var(--sab);} body.chat-open .content .panel{bottom:var(--sab);}
/* Native-style push/pop: the conversation slides in from the right when opened and slides back out
when you go back, so navigation has a clear direction (like Teams/WhatsApp). */
@keyframes bzConvoIn{from{transform:translateX(100%);}to{transform:translateX(0);}}
@keyframes bzConvoOut{from{transform:translateX(0);}to{transform:translateX(100%);}}
body.chat-open:not(.chat-closing) .content .panel{animation:bzConvoIn .22s cubic-bezier(.2,.7,.3,1);}
body.chat-closing .content{display:block!important;}
body.chat-closing .content .panel{animation:bzConvoOut .2s cubic-bezier(.4,0,.7,.4) forwards;will-change:transform;}
/* Bigger touch targets */ /* Bigger touch targets */
.chat-row{padding:.7rem .85rem;} .chat-row{padding:.7rem .85rem;}
.convo-head{padding:.7rem .85rem;} .convo-head{padding:.7rem .85rem;}
@@ -1095,7 +1101,7 @@
</head> </head>
<body> <body>
<script src="/icons.js?v=6"></script> <script src="/icons.js?v=6"></script>
<script>window.__BUILD='2026-07-17-batch91';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD); <script>window.__BUILD='2026-07-17-batch92';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
// Emoji are rendered with the OS's own (colour) emoji font — instant, zero network. // Emoji are rendered with the OS's own (colour) emoji font — instant, zero network.
// //
// We used to run Twemoji over every emoji, which swapped each one for an <img> pulled INDIVIDUALLY from // We used to run Twemoji over every emoji, which swapped each one for an <img> pulled INDIVIDUALLY from
@@ -1105,16 +1111,11 @@
// existing call sites don't have to change. // existing call sites don't have to change.
function twemojify(_el){ /* native emoji: nothing to do */ }</script> function twemojify(_el){ /* native emoji: nothing to do */ }</script>
<script> <script>
// Native shell chrome (iOS/Android). The Capacitor WebView reports env(safe-area-inset-*)=0, so content // Native shell tag (iOS/Android). Safe-area insets are handled natively by the @capacitor-community/
// slides under the Dynamic Island / notch. We tag <html> (CSS then hard-codes the bottom home-indicator // safe-area plugin (it makes env(safe-area-inset-*) report real values), so there's no JS status-bar
// inset) and ask the StatusBar plugin to NOT overlay the WebView, which pushes the top of our content // juggling here — we only tag <html> so CSS can make small native-only tweaks if needed.
// below the status bar. Runs on every entry page (home/share/connect/index) that includes this snippet. (function(){ try{ var C=window.Capacitor; if(!C||!C.isNativePlatform||!C.isNativePlatform()) return;
(function(){ try{ var p=(C.getPlatform&&C.getPlatform())||''; var r=document.documentElement; r.classList.add('native-app'); if(p) r.classList.add('native-'+p);
var C=window.Capacitor; if(!C||!C.isNativePlatform||!C.isNativePlatform()) return;
var p=(C.getPlatform&&C.getPlatform())||''; var r=document.documentElement;
r.classList.add('native-app'); if(p) r.classList.add('native-'+p);
var SB=C.Plugins&&C.Plugins.StatusBar;
if(SB){ try{ SB.setOverlaysWebView({overlay:false}); }catch(_){}; try{ SB.setStyle({style:'DARK'}); }catch(_){}; try{ SB.setBackgroundColor({color:'#ffffff'}); }catch(_){} }
}catch(_){} })(); }catch(_){} })();
</script> </script>
<div class="loading" id="loading"><div class="ld-inner"><img src="/loaders/loader-orbit-dark.svg" alt="" width="72" height="72"><div class="ld-wm">Biz <b>Connect</b></div><span class="ld-sub">Loading…</span></div></div> <div class="loading" id="loading"><div class="ld-inner"><img src="/loaders/loader-orbit-dark.svg" alt="" width="72" height="72"><div class="ld-wm">Biz <b>Connect</b></div><span class="ld-sub">Loading…</span></div></div>
@@ -4613,7 +4614,15 @@ function switchTab(tab){
if(tab==='connect' && !loaded.connect){ document.getElementById('connectPanel').innerHTML='<iframe src="/connect?embed=1" allow="camera; microphone; display-capture; clipboard-read; clipboard-write"></iframe>'; loaded.connect=true; } if(tab==='connect' && !loaded.connect){ document.getElementById('connectPanel').innerHTML='<iframe src="/connect?embed=1" allow="camera; microphone; display-capture; clipboard-read; clipboard-write"></iframe>'; loaded.connect=true; }
if(tab==='meeting' && meetState==='idle'){ renderMeetingLobby(); } if(tab==='meeting' && meetState==='idle'){ renderMeetingLobby(); }
} }
function showWelcome(){ selected=null; document.body.classList.remove('chat-open'); renderChats(searchVal()); renderChatPanel(); updateRailUnread(); } function showWelcome(){
const finish=()=>{ selected=null; document.body.classList.remove('chat-open','chat-closing'); renderChats(searchVal()); renderChatPanel(); updateRailUnread(); };
// Mobile: play the slide-out (bzConvoOut) before swapping back to the chat list, so "back" has a
// clear push/pop direction. Desktop just closes instantly.
if(document.body.classList.contains('chat-open') && !document.body.classList.contains('chat-closing') && window.matchMedia('(max-width:760px)').matches){
document.body.classList.add('chat-closing');
setTimeout(finish, 200);
} else finish();
}
railBtns.forEach(btn=>{ btn.onclick=()=>{ railBtns.forEach(btn=>{ btn.onclick=()=>{
const tab=btn.dataset.tab; const tab=btn.dataset.tab;
// Re-clicking Chat (while already on it) returns to the welcome screen. // Re-clicking Chat (while already on it) returns to the welcome screen.
-9
View File
@@ -3,15 +3,6 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Biz Connect</title>
<script>
// Native shell: push content below the status bar/notch (the Capacitor WebView reports safe-area insets
// as 0). Runs before render so headers never sit under the Dynamic Island.
(function(){ try{ var C=window.Capacitor; if(!C||!C.isNativePlatform||!C.isNativePlatform()) return;
var p=(C.getPlatform&&C.getPlatform())||''; var r=document.documentElement; r.classList.add('native-app'); if(p) r.classList.add('native-'+p);
var SB=C.Plugins&&C.Plugins.StatusBar; if(SB){ try{ SB.setOverlaysWebView({overlay:false}); }catch(_){}; try{ SB.setStyle({style:'DARK'}); }catch(_){}; try{ SB.setBackgroundColor({color:'#ffffff'}); }catch(_){} }
}catch(_){} })();
</script>
<meta name="theme-color" content="#1F3B73"> <meta name="theme-color" content="#1F3B73">
<link rel="icon" href="/favicon.ico?v=3" sizes="any"> <link rel="icon" href="/favicon.ico?v=3" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png?v=3"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png?v=3">
+1 -9
View File
@@ -4,14 +4,6 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Biz Connect — Share your screen</title> <title>Biz Connect — Share your screen</title>
<script>
// Native shell: push content below the status bar/notch (the Capacitor WebView reports safe-area insets
// as 0, so the back button/header would otherwise slide under the Dynamic Island).
(function(){ try{ var C=window.Capacitor; if(!C||!C.isNativePlatform||!C.isNativePlatform()) return;
var p=(C.getPlatform&&C.getPlatform())||''; var r=document.documentElement; r.classList.add('native-app'); if(p) r.classList.add('native-'+p);
var SB=C.Plugins&&C.Plugins.StatusBar; if(SB){ try{ SB.setOverlaysWebView({overlay:false}); }catch(_){}; try{ SB.setStyle({style:'DARK'}); }catch(_){}; try{ SB.setBackgroundColor({color:'#ffffff'}); }catch(_){} }
}catch(_){} })();
</script>
<meta name="theme-color" content="#1F3B73"> <meta name="theme-color" content="#1F3B73">
<link rel="icon" href="/favicon.ico?v=3" sizes="any"> <link rel="icon" href="/favicon.ico?v=3" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png?v=3"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png?v=3">
@@ -85,7 +77,7 @@
<body> <body>
<script>if(new URLSearchParams(location.search).get('embed')==='1')document.documentElement.classList.add('embed');</script> <script>if(new URLSearchParams(location.search).get('embed')==='1')document.documentElement.classList.add('embed');</script>
<div class="indicator" id="indicator">● Your screen is being shared — close this tab anytime to stop</div> <div class="indicator" id="indicator">● Your screen is being shared — close this tab anytime to stop</div>
<a href="/" id="homeLink" style="position:fixed;top:14px;left:16px;z-index:50;display:inline-flex;align-items:center;gap:6px;background:rgba(255,255,255,.92);color:#1F3B73;text-decoration:none;font-weight:600;font-size:.86rem;padding:.45rem .8rem;border-radius:10px;box-shadow:0 4px 12px rgba(0,0,0,.15)"><span data-ic="arrowLeft" data-sz="16"></span> Home</a> <a href="/" id="homeLink" style="position:fixed;top:calc(14px + env(safe-area-inset-top,0px));left:calc(16px + env(safe-area-inset-left,0px));z-index:50;display:inline-flex;align-items:center;gap:6px;background:rgba(255,255,255,.92);color:#1F3B73;text-decoration:none;font-weight:600;font-size:.86rem;padding:.45rem .8rem;border-radius:10px;box-shadow:0 4px 12px rgba(0,0,0,.15)"><span data-ic="arrowLeft" data-sz="16"></span> Home</a>
<div class="stage"> <div class="stage">
<div class="brandpanel"> <div class="brandpanel">
<img src="/icon-512.png" alt="" style="width:88px;height:88px;border-radius:22px;object-fit:contain;margin-bottom:1.2rem;background:#fff;padding:8px;box-shadow:0 12px 30px rgba(0,0,0,.25)" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'mark',textContent:'B'}))"> <img src="/icon-512.png" alt="" style="width:88px;height:88px;border-radius:22px;object-fit:contain;margin-bottom:1.2rem;background:#fff;padding:8px;box-shadow:0 12px 30px rgba(0,0,0,.25)" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'mark',textContent:'B'}))">