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 {