From 10b2251efee3a762f47ad327fbc70493a0437dd3 Mon Sep 17 00:00:00 2001 From: sravan Date: Wed, 19 Aug 2026 19:51:25 +0530 Subject: [PATCH] Scheduled meetings: deliver a background push (fixes no iOS notification) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: the scheduled-meeting INVITE (routes.js) and the ~10-min REMINDER (reminders.js) both notified only via CHAT.pushToUser — the WebSocket channel, which only reaches an OPEN tab with a live socket. Neither called PUSH.sendToUser, the path that produces a background APNs/FCM/web-push alert. On iOS the webview is suspended in the background, so the WS event was simply missed and no notification appeared. (Chat messages already call PUSH.sendToUser, which is why chat notices arrive on iOS but meeting ones didn't.) Fix: - schedule invite: also PUSH.sendToUser to every invited participant + group members (kind:'meeting', id:roomCode) so a closed app is notified. - reminders.js: also PUSH.sendToUser to all reminder recipients. - client: a kind:'meeting' notification tap now opens the Meeting tab + its list (both the live-tab open-chat handler and the cold-boot openKind path), instead of calling selectChat with an unsupported kind. Also: APPSTORE_SUBMISSION.md §8 updated — the UGC Report/Block gate (guideline 1.2) is now implemented, with a suggested reviewer note. Co-Authored-By: Claude Opus 4.8 --- mobile/APPSTORE_SUBMISSION.md | 156 ++++++++++++++++++++++++++++++++++ server/public/home.html | 9 +- server/reminders.js | 4 +- server/routes.js | 7 ++ 4 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 mobile/APPSTORE_SUBMISSION.md diff --git a/mobile/APPSTORE_SUBMISSION.md b/mobile/APPSTORE_SUBMISSION.md new file mode 100644 index 0000000..7226ec2 --- /dev/null +++ b/mobile/APPSTORE_SUBMISSION.md @@ -0,0 +1,156 @@ +# Biz Connect — App Store submission pack + +Everything App Store Connect asks for, drafted. Fill the **`<< … >>`** placeholders (they're +account-specific or secret and must NOT be committed). Order below ≈ the order App Store Connect walks you through. + +--- + +## 0. Before you submit (gates that cause rejection) + +- [ ] **Verified build** uploaded from Codemagic to App Store Connect (the one you tested on-device — multi-device tiles, call transcript, scheduled-meeting transcript, calls still ring). +- [ ] **Reviewer demo account** created and working (§6). The app is gated behind BizGaze login — without working credentials Apple auto-rejects ("couldn't get past sign-in"). +- [ ] **Privacy Policy URL** live and reachable (§4 needs it). +- [ ] **Screenshots** captured for the required sizes (§5). +- [ ] **Export compliance** answered (§7). + +--- + +## 1. App information + +| Field | Value | +|---|---| +| **App name** | Biz Connect | +| **Subtitle** (30 char max) | Team chat, calls & meetings | +| **Primary category** | Business | +| **Secondary category** | Productivity | +| **Bundle ID** | com.bizgaze.connect | +| **Support URL** | `<< https://…/support >>` | +| **Marketing URL** (optional) | `<< https://… >>` | +| **Age rating** | 4+ (answer all content questions "None". Note: user-generated content via chat — see §8) | + +--- + +## 2. Description + +> Biz Connect keeps your team connected — chat, voice and video calls, and meetings, in one place. +> +> **Chat that works the way your team does** +> • Direct messages and group conversations +> • Reactions, replies, mentions, pinned messages, and polls +> • Share photos, videos, and files +> • Read receipts and typing indicators +> +> **Calls that ring like a real phone** +> • One-to-one and group voice & video calls +> • Full-screen incoming call ringing, even when the app is closed +> • Calls keep working when you switch apps or lock your phone +> +> **Meetings, built in** +> • Start instantly or schedule ahead +> • Screen sharing and camera, front or back +> • Live transcripts you can save and download +> • Meeting recordings for later +> +> **Everywhere you are** +> Your conversations stay in sync across iPhone, desktop, and the web. +> +> Biz Connect is for organizations using the BizGaze platform. Sign in with your BizGaze account to get started. + +**Keywords** (100 char max, comma-separated, no spaces after commas): +`team chat,business messaging,video call,voice call,meetings,screen share,transcript,collaboration,work` + +**Promotional text** (170 char, editable without a new build): +> Chat, call, and meet with your team — with real-phone-style ringing, screen sharing, and live meeting transcripts. + +--- + +## 3. What's New (release notes for this version) + +> • Live meeting transcripts on iPhone — for both calls and scheduled meetings +> • Join the same meeting from two devices at once, each as its own participant +> • Stability and audio-routing improvements + +--- + +## 4. App Privacy ("nutrition label") + +Answer these in App Store Connect → App Privacy. **Verify each against what the BizGaze backend actually stores** +before publishing — this is a legal declaration. Sensible defaults for a business comms app: + +**Data used to identify the user (Linked to identity):** +- **Contact Info → Name, Email address** — App Functionality, Account management. (BizGaze login.) +- **User Content → Photos or Videos, Other User Content (messages, files)** — App Functionality. (Chat/meeting content stored on your server.) +- **Identifiers → User ID** — App Functionality. + +**Diagnostics / Usage:** declare only if you actually collect analytics/crash data. If not, mark **"Data Not Collected"** for those. + +**Important clarifications to make in the notes:** +- **Microphone & Camera** audio/video for calls is transmitted between participants (via your LiveKit server) but is only *recorded/stored* when a user explicitly starts a recording or transcript. Say so. +- **Speech recognition** for transcripts runs **on-device** (Apple's `SFSpeechRecognizer`, on-device mode) — the audio is not sent to Apple, and only the finished text is added to the meeting transcript. This is a good thing to state explicitly; it reassures review. +- **Third-party:** if BizGaze/LiveKit are your own infrastructure, no third-party SDK data-sharing to declare. Confirm you have no analytics/ad SDKs. + +**Privacy usage strings** (already in the build via `ios-patch.sh` — for reference): +- Camera: "Biz Connect uses the camera for video calls and to share photos and your screen." +- Microphone: "Biz Connect uses the microphone for voice and video calls." +- Speech Recognition: "Biz Connect uses speech recognition to create live meeting transcripts from your microphone." +- Photo Library / Add: send/save images. + +--- + +## 5. Screenshots + +Required (App Store Connect accepts one size and scales, but do at least these two): +- [ ] **6.9" iPhone** (1320 × 2868) — iPhone 16 Pro Max class +- [ ] **6.5" iPhone** (1242 × 2688) — fallback for older devices +- [ ] (Optional) iPad if you enable iPad support + +Suggested 4–5 shots, in order: **chat list → a conversation → an active video call → screen share / meeting → live transcript**. Use realistic but non-sensitive demo content. + +--- + +## 6. App Review notes (paste into "Notes") + +> Biz Connect requires a BizGaze account to sign in. +> +> Demo account for review: +> Email: << demo@yourdomain >> +> Password: << demo password >> +> +> How to test: +> 1. Open the app and sign in with the demo account above. +> 2. Chat tab: open a conversation to see messaging. +> 3. Start a call from a conversation, or the Meetings tab to start/join a meeting. +> 4. In a meeting, tap "Live transcript" to see on-device speech-to-text. +> +> Notes on permissions: +> • Microphone/Camera — used for voice and video calls. +> • Speech Recognition — used only to generate live meeting transcripts; recognition runs on-device. +> • Screen recording (broadcast) — used only when the user chooses to share their screen in a meeting. +> • VoIP push (PushKit) + CallKit — used to ring incoming calls like a normal phone call. + +**Create the demo account now** and confirm it can actually log in and start a call. A dead demo login is the #1 rejection cause for account-gated apps. + +--- + +## 7. Export compliance + +The app uses only standard encryption (HTTPS/TLS, WebRTC/DTLS-SRTP) — no proprietary/custom crypto. +- In App Store Connect: **"Does your app use encryption?" → Yes**, then **"only … standard encryption algorithms" → Yes** → qualifies for the exemption (no CCATS/year-end self-classification report needed for standard encryption). +- Optional: set `ITSAppUsesNonExemptEncryption = NO` in Info.plist to skip the question each submission (add to `ios-patch.sh` if you want it permanent — say the word and I'll add it). + +--- + +## 8. Likely review questions / risks (and answers) + +- **Account-gated app** → mitigated by the demo account (§6). Also fine per guideline 3.1.1 since it's a business tool, not gating features behind sign-in for a consumer app. +- **User-generated content (chat)** → guideline 1.2 satisfied (shipped 2026-08-19): every message has **Report** (long-press / ⋮ → Report, canned reasons) and **Block user**; blocked users can't message or call you (server-enforced). A **Blocked users** manager lives in the profile menu (unblock anytime), and workspace **admins** get a **Reported messages** review screen (delete content / block / resolve). Reports are org-internal (routed to the workspace's own admins). Reviewer note suggestion: "Report and Block are available on any message via long-press; Blocked users are managed from the profile menu." +- **CallKit + VoIP push** → legitimate; the demo/reviewer flow should show a real incoming call if possible. +- **Background modes** (audio, voip) → justified by calls; the review notes cover it. + +--- + +## 9. Nice-to-haves (not blockers) + +- App Store promotional/preview **video** (optional). +- Localized metadata if you target non-English regions. +- A short **"in-app account deletion"** path — Apple requires apps with account creation to offer account deletion (guideline 5.1.1(v)). If BizGaze accounts are created/managed externally (admin-provisioned, not self-signup in the app), note that in review; if users *can* self-register in the app, an in-app "delete my account" (or a clear link to do so) is required. diff --git a/server/public/home.html b/server/public/home.html index 3daabd9..5a8d440 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -4222,7 +4222,7 @@ async function setupPush(){ try{ _swReg=await navigator.serviceWorker.ready; }catch(e){ console.warn('[push] SW never became ready:', e); return; } // ensure an ACTIVE worker before subscribe() console.log('[push] service worker ready'); // Clicking an OS notification asks us (if a tab is already open) to open that chat in place. - try{ navigator.serviceWorker.addEventListener('message',(e)=>{ const d=e.data||{}; if(d.type==='open-chat' && d.id){ try{ selectChat(d.kind||'dm', d.id); }catch(_){} } }); }catch(_){} + try{ navigator.serviceWorker.addEventListener('message',(e)=>{ const d=e.data||{}; if(d.type==='open-chat' && d.id){ if((d.kind||'')==='meeting'){ try{ switchTab('meeting'); if(typeof loadScheduledMeetings==='function') loadScheduledMeetings(); }catch(_){} } else { try{ selectChat(d.kind||'dm', d.id); }catch(_){} } } }); }catch(_){} await subscribePush(); } async function subscribePush(){ @@ -6570,7 +6570,12 @@ window.addEventListener('message',(e)=>{ // thread fetch) and load the sidebar in the background — don't make the reload wait on it. let oid=null, okind='dm'; try{ const q=new URLSearchParams(location.search); oid=q.get('openId'); okind=q.get('openKind')||'dm'; if(oid) history.replaceState(null,'','/home'); }catch(_){} - if(oid){ + if(oid && okind==='meeting'){ + // Opened from a scheduled-meeting notification → jump to the Meeting tab + its list (not a chat). + document.getElementById('loading').style.display='none'; + switchTab('meeting'); try{ if(typeof loadScheduledMeetings==='function') loadScheduledMeetings(); }catch(_){} + loadSidebar(); + } else if(oid){ selectChat(okind, oid); // open the chat right away (only waits on the thread fetch) document.getElementById('loading').style.display='none'; // the chat pane shows its own thread loader loadSidebar().then(()=>{ try{ const it=rowFor(okind,oid); const nm=document.querySelector('#chatPanel .convo-head .nm'); if(it&&nm) nm.textContent=it.name; }catch(_){} }); diff --git a/server/reminders.js b/server/reminders.js index 07a6f88..d27088b 100644 --- a/server/reminders.js +++ b/server/reminders.js @@ -2,6 +2,7 @@ // group members, and invited participants. Runs on a 60s tick; marks each meeting reminded. const R = require('./repos'); const CHAT = require('./chat'); +const PUSH = require('./push'); // native/web background push so a CLOSED app still gets the reminder async function tick() { try { @@ -13,7 +14,8 @@ async function tick() { invited.forEach((id) => recipients.add(id)); if (s.group_id) { try { (await R.conversations.members(s.group_id)).forEach((m) => recipients.add(m)); } catch (_) {} } const evt = { type: 'meeting-reminder', meeting: { id: s.id, title: s.title, scheduledAt: s.scheduled_at, room: s.room_code } }; - recipients.forEach((uid) => { try { CHAT.pushToUser(uid, evt); } catch (_) {} }); + recipients.forEach((uid) => { try { CHAT.pushToUser(uid, evt); } catch (_) {} }); // open tab + recipients.forEach((uid) => { try { PUSH.sendToUser(uid, { title: 'Meeting starting soon', body: (s.title || 'Your meeting') + ' starts in ~10 minutes', kind: 'meeting', id: s.room_code, tag: 'meet:' + s.room_code }); } catch (_) {} }); // closed app (iOS APNs etc.) await R.scheduledMeetings.markReminded(s.id); } } catch (_) { /* never let the timer die */ } diff --git a/server/routes.js b/server/routes.js index 38a1bea..baf3cd6 100644 --- a/server/routes.js +++ b/server/routes.js @@ -1267,6 +1267,13 @@ route('POST', '/api/meetings/schedule', async (req, res) => { // Invitation notification to each invited participant. const inviteEvt = { type: 'meeting-invite', meeting: { id, title: t, scheduledAt: when, whenText: label, room: code, by: u.name || u.email } }; for (const pid of invited) { try { CHAT.pushToUser(pid, inviteEvt); } catch (_) {} } + // Background/native push (iOS APNs, Android FCM, web push) so a CLOSED app still gets the invite. The + // CHAT.pushToUser above only reaches an OPEN tab with a live socket — which is exactly why scheduled- + // meeting notices never arrived on iOS (the webview is suspended in the background). Mirror the chat path. + const notifyIds = new Set(invited); + if (groupId) { try { (await R.conversations.members(groupId)).forEach((m) => notifyIds.add(m)); } catch (_) {} } + notifyIds.delete(u.id); + for (const pid of notifyIds) { try { PUSH.sendToUser(pid, { title: (u.name || u.email) + ' scheduled a meeting', body: t + ' · ' + label, kind: 'meeting', id: code, tag: 'meet:' + code }); } catch (_) {} } // Email invites (#4): the guest join link goes to external invitees, plus any invited Connect users // who have an email on file. Fire-and-forget — a mail outage never fails scheduling. No-op if SMTP off. try {