feat(mobile): add local QR scanning and desktop pairing invitations
This commit is contained in:
1 parent
c1929957a2
commit
4115f1b4c0
17 files changed
+178
-17
No files matched your search
+23
-2
@@ -1,6 +1,8 @@
|
||||
import { AccountClient, openPhoneConnection } from './transport.mjs'
|
||||
import { VoiceSession } from './voice.mjs'
|
||||
import { taskState } from './behavior.mjs'
|
||||
import { parseInvite } from './invite.mjs'
|
||||
import { InviteScanner } from './qr.mjs'
|
||||
const $ = id => document.getElementById(id)
|
||||
if (globalThis.Capacitor?.isNativePlatform()) globalThis.Capacitor.registerPlugin('MobileSecrets')
|
||||
let credentials = {}, account, connection, selected, approval, voice, pendingTask, usageTimer, voiceGeneration = 0
|
||||
@@ -8,6 +10,23 @@ let connectedDesktopId = null
|
||||
let connectionEpoch = 0, selectionEpoch = 0
|
||||
const status = text => { $('status').textContent = text }
|
||||
const native = () => window.Capacitor?.Plugins?.MobileSecrets
|
||||
const scanner = new InviteScanner($('qr-video'), async raw => {
|
||||
if (!account || credentials.deviceId) throw new Error('Önce giriş yapın; kayıtlı cihaz varsa önce mevcut eşleşmeyi kontrol edin.')
|
||||
const targetAccount = account, epoch = connectionEpoch
|
||||
const invite = parseInvite(raw)
|
||||
const result = await account.call(`/v1/aiturk/mobile/invites/${invite.inviteId}/claim`, {
|
||||
secret: invite.secret, display_name: $('device-name').value.trim() })
|
||||
if (account !== targetAccount || epoch !== connectionEpoch) return
|
||||
Object.assign(credentials, { deviceId: result.device_id, deviceToken: result.device_token,
|
||||
pairingId: result.pairing_id, preferredDesktopId: invite.desktopId })
|
||||
await saveCredentials()
|
||||
$('pair-code').textContent = `Bilgisayarda bekleyen telefonu getirip şu kodu onaylayın: ${result.user_code}`
|
||||
status('QR okundu. Bilgisayardaki açık telefon onayı bekleniyor.')
|
||||
})
|
||||
scanner.onError = error => status(error.message)
|
||||
action('scan-invite', async () => { if (!account || credentials.deviceId) throw new Error('Önce giriş yapın; bu telefon henüz eşleştirilmemiş olmalı.'); await scanner.start() })
|
||||
action('stop-scan', async () => scanner.stop())
|
||||
$('qr-file').onchange = async event => { try { await scanner.image(event.target.files[0]) } catch (error) { status(error.message) } finally { event.target.value = '' } }
|
||||
async function saveCredentials() { if (native()) await native().save({ value: JSON.stringify(credentials) }) }
|
||||
function addText(container, role, text) { const p = document.createElement('p'); p.textContent = `${role}: ${text}`; $(container).append(p); while ($(container).children.length > 150) $(container).firstChild.remove() }
|
||||
function action(id, work) { $(id).onclick = async () => { $(id).disabled = true; try { await work() } catch (error) { status(error.message) } finally { $(id).disabled = false } } }
|
||||
@@ -41,6 +60,7 @@ action('login-start', async () => {
|
||||
status('Tarayıcıda giriş yaptıktan sonra buraya dönüp girişi kontrol edin.')
|
||||
})
|
||||
action('login-check', async () => {
|
||||
if (credentials.accountToken) throw new Error('Hesaba zaten giriş yapılmış; mevcut hesap değiştirilmeyecek.')
|
||||
if (!credentials.pendingLogin) throw new Error('Önce giriş başlatın.')
|
||||
const result = await new AccountClient({ accountToken: credentials.pendingLogin.deviceCode }).call('/v1/aiturk/mobile-auth/poll', {})
|
||||
if (result.state !== 'approved') { status('Tarayıcıdaki kod onayı bekleniyor.'); return }
|
||||
@@ -49,6 +69,7 @@ action('login-check', async () => {
|
||||
status('Hesap bağlı. Şimdi telefonun eşleştirme kodunu oluşturun.')
|
||||
})
|
||||
action('logout', async () => {
|
||||
scanner.stop()
|
||||
const token = credentials.accountToken || credentials.pendingLogin?.deviceCode
|
||||
// Revoke the app-specific credential before dropping the local copy. A
|
||||
// network failure leaves it available for an explicit retry.
|
||||
@@ -166,6 +187,6 @@ action('history', async () => { const result = await rpc('session.history', { se
|
||||
action('clear-history', async () => { $('discussion').replaceChildren(); $('result').replaceChildren(); status('Telefondaki görünür metin temizlendi. Google bağlantı bağlamını da temizlemek için görüşmeyi kapatın.') })
|
||||
action('disconnect', async () => { connectionEpoch++; stopVoice(); connection?.close(); connection = null; selected = null; $('work').hidden = true; status('Telefon bağlantısı kapalı. IDE işi devam edebilir.') })
|
||||
action('revoke', async () => { const data = await account.call('/v1/aiturk/mobile/devices'); const self = data.devices.find(d => d.self); await account.call(`/v1/aiturk/devices/${self.id}/revoke`, { expected_version: self.version }); stopVoice(); connection?.close(); connection = null; credentials = {}; await saveCredentials(); account = null; $('work').hidden = true; status('Bu telefonun erişimi iptal edildi.') })
|
||||
document.addEventListener('visibilitychange', () => { if (document.hidden) stopVoice() })
|
||||
window.addEventListener('pagehide', () => { stopVoice(); connection?.close() })
|
||||
document.addEventListener('visibilitychange', () => { if (document.hidden) { stopVoice(); scanner.stop() } })
|
||||
window.addEventListener('pagehide', () => { stopVoice(); scanner.stop(); connection?.close() })
|
||||
try { const saved = await native()?.load(); if (saved?.value) { credentials = JSON.parse(saved.value); if (credentials.accountToken) { account = new AccountClient(credentials); status('Kayıtlı cihaz bulundu. Bilgisayarları yenileyerek bağlanın.') } } } catch { status('Android güvenli kasası açılamadı. Hesabınızı yeniden eşleştirin.') }
|
||||
Reference in new issue
Block a user