feat(share): send from inside the share sheet — the Teams-style in-extension picker
Replaces the wrong approach (stage + try to bounce into the app, which iOS blocks) with the one Teams/WhatsApp actually use: the picker and the send happen INSIDE the share extension, so there's no app-open at all. Tap Share → Biz Connect → pick a chat → it uploads and sends, right there in the sheet. How the extension can send without the app: it's a separate process that can't see the web app's HttpOnly cookie, so: - server: GET /api/share/token mints a bearer token for the logged-in user. - web: on every launch the app fetches that token and hands it to the extension via the App Group (ShareInbox.setAuth writes token+base to the shared UserDefaults). - extension: reads the token and calls the SAME API the native client uses — GET /api/messages/conversations to list chats, POST /api/messages/upload for each file, POST /api/messages to send. Native UITableView picker with search. Robustness: it still stages the files + writes a manifest first, so if there's no token yet (user never signed in) or the send fails, the file isn't lost — the app collects it on next open, exactly as before. On success the manifest is cleared so the app doesn't re-offer it. Server + web are live now; the token endpoint is harmless until a build ships the extension. NEEDS A NEW iOS BUILD for the picker itself. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3528,6 +3528,17 @@ async function sendMessage(){
|
||||
// build) or there's nothing pending, this is a no-op.
|
||||
let _shareBusy=false;
|
||||
function bzShareInbox(){ const P=window.Capacitor&&window.Capacitor.Plugins; return P&&P.ShareInbox||null; }
|
||||
// Hand the Share Extension a bearer token + API base so it can list chats, upload and send on its own —
|
||||
// the Teams-style in-sheet picker, no app-open needed. The extension can't read our HttpOnly cookie, so
|
||||
// the logged-in web app mints a token (/api/share/token) and writes it into the App Group each launch.
|
||||
async function bzPushShareAuth(){
|
||||
const SI=bzShareInbox(); if(!SI||!SI.setAuth||!ME||!ME.id) return;
|
||||
try{
|
||||
const r=await fetch('/api/share/token'); if(!r.ok) return;
|
||||
const d=await r.json(); if(!d||!d.token) return;
|
||||
await SI.setAuth({ token:d.token, base:location.origin });
|
||||
}catch(_){}
|
||||
}
|
||||
async function bzCheckSharedInbox(){
|
||||
const SI=bzShareInbox(); if(!SI||_shareBusy) return;
|
||||
if(!ME||!ME.id) return; // must be signed in to choose a conversation
|
||||
@@ -5663,6 +5674,8 @@ window.addEventListener('message',(e)=>{
|
||||
// Cold launch FROM the share sheet: the extension staged files before the app was even running, so
|
||||
// the appUrlOpen listener may have missed it. Sweep once now that ME + the chat list are ready.
|
||||
setTimeout(bzCheckSharedInbox, 600);
|
||||
// Refresh the token the Share Extension uses to send on its own (Teams-style in-sheet picker).
|
||||
bzPushShareAuth();
|
||||
})();
|
||||
|
||||
// GUEST meeting: a lightweight pre-join (name) then join the call with a throwaway guest identity —
|
||||
|
||||
@@ -304,6 +304,19 @@ route('POST', '/api/auth/refresh', async (req, res) => {
|
||||
json(res, 200, { ok: true, token: tok, expiresAt: now() + SESSION_TTL, refreshToken: newRefresh, refreshExpiresAt: now() + REFRESH_TTL });
|
||||
});
|
||||
|
||||
// Mint a bearer token for the iOS Share Extension. The extension is a separate process that can't see the
|
||||
// web app's HttpOnly `sid` cookie, so the logged-in web app calls this on boot and hands the token to the
|
||||
// extension via the App Group. The extension then talks to the API directly (list chats, upload, send) —
|
||||
// exactly like the native client, so no app-open is needed to share. Short-ish TTL, refreshed each boot.
|
||||
route('GET', '/api/share/token', async (req, res) => {
|
||||
const u = currentUser(req);
|
||||
if (!u) return json(res, 401, { error: 'unauthorized' });
|
||||
const tok = A.token();
|
||||
const ttl = 1000 * 60 * 60 * 24 * 30; // 30 days; the app re-mints on every launch anyway
|
||||
R.authSessions.create({ token: tok, userId: u.id, mfaPassed: true, ttl });
|
||||
json(res, 200, { token: tok, expiresAt: now() + ttl });
|
||||
});
|
||||
|
||||
// Login step 2: TOTP code -> marks session mfa_passed
|
||||
route('POST', '/api/login/mfa', async (req, res) => {
|
||||
const { code } = await readBody(req);
|
||||
|
||||
Reference in New Issue
Block a user