From a53db6eafe6031ed43bad70b8154adbd30a53975 Mon Sep 17 00:00:00 2001 From: dranik Date: Fri, 8 May 2026 10:21:24 +0300 Subject: [PATCH] fixed open QR code screen & fix iOS scanner --- client/platforms/ios/QRCodeReaderBase.mm | 21 +++-- .../controllers/api/pairingUiController.cpp | 22 +++++ .../ui/controllers/api/pairingUiController.h | 2 + .../ui/qml/Pages2/PageSettingsApiDevices.qml | 9 +- .../Pages2/PageSetupWizardConfigSource.qml | 7 +- tools/local_gateway/main.go | 89 ++++++++++++++++--- 6 files changed, 129 insertions(+), 21 deletions(-) diff --git a/client/platforms/ios/QRCodeReaderBase.mm b/client/platforms/ios/QRCodeReaderBase.mm index e865b9c16..91717d057 100644 --- a/client/platforms/ios/QRCodeReaderBase.mm +++ b/client/platforms/ios/QRCodeReaderBase.mm @@ -14,6 +14,7 @@ @property (nonatomic) QRCodeReader* qrCodeReader; @property (nonatomic, strong) AVCaptureSession *captureSession; @property (nonatomic, strong) AVCaptureVideoPreviewLayer *videoPreviewPlayer; +@property (nonatomic) dispatch_queue_t sessionQueue; @end @@ -23,6 +24,9 @@ [super viewDidLoad]; _captureSession = nil; + if (!_sessionQueue) { + _sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL); + } } - (void)setQrCodeReader: (QRCodeReader*)value { @@ -48,9 +52,10 @@ AVCaptureMetadataOutput *capturedMetadataOutput = [[AVCaptureMetadataOutput alloc] init]; [_captureSession addOutput:capturedMetadataOutput]; - dispatch_queue_t dispatchQueue; - dispatchQueue = dispatch_queue_create("myQueue", NULL); - [capturedMetadataOutput setMetadataObjectsDelegate: self queue: dispatchQueue]; + if (!_sessionQueue) { + _sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL); + } + [capturedMetadataOutput setMetadataObjectsDelegate: self queue: _sessionQueue]; [capturedMetadataOutput setMetadataObjectTypes: [NSArray arrayWithObject:AVMetadataObjectTypeQRCode]]; _videoPreviewPlayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession: _captureSession]; @@ -69,7 +74,10 @@ CALayer* layer = [UIApplication sharedApplication].keyWindow.layer; [layer addSublayer: _videoPreviewPlayer]; - [_captureSession startRunning]; + AVCaptureSession *session = _captureSession; + dispatch_async(_sessionQueue, ^{ + [session startRunning]; + }); NSLog(@"[QRCodeReader] startReading OK frame=(%.1f,%.1f,%.1f,%.1f) statusBar=%.1f", cameraCGRect.origin.x, cameraCGRect.origin.y, cameraCGRect.size.width, cameraCGRect.size.height, statusBarHeight); @@ -79,7 +87,10 @@ - (void)stopReading { if (_captureSession) { - [_captureSession stopRunning]; + AVCaptureSession *session = _captureSession; + dispatch_async(_sessionQueue, ^{ + [session stopRunning]; + }); _captureSession = nil; } if (_videoPreviewPlayer) { diff --git a/client/ui/controllers/api/pairingUiController.cpp b/client/ui/controllers/api/pairingUiController.cpp index 4f58c0005..753f2298f 100644 --- a/client/ui/controllers/api/pairingUiController.cpp +++ b/client/ui/controllers/api/pairingUiController.cpp @@ -25,7 +25,9 @@ namespace { constexpr auto kGenerateQrPath = "%1api/v1/generate_qr"; constexpr auto kScanQrPath = "%1api/v1/scan_qr"; +constexpr auto kGatewayProbePath = "%1v1/news"; constexpr int kPairingRetryMaxAttempts = 3; +constexpr int kGatewayProbeTimeoutMsecs = 3000; bool isPairingRetriableError(ErrorCode code) { @@ -282,6 +284,26 @@ void PairingUiController::setPhoneBusy(bool busy) emit phonePairingBusyChanged(); } +bool PairingUiController::canOpenTvQrPairingPage() +{ + if (!m_appSettingsRepository) { + emit errorOccurred(ErrorCode::InternalError); + return false; + } + + const bool isTestPurchase = false; + GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase), + m_appSettingsRepository->isDevGatewayEnv(isTestPurchase), kGatewayProbeTimeoutMsecs, + m_appSettingsRepository->isStrictKillSwitchEnabled()); + QByteArray responseBody; + const ErrorCode err = gatewayController.post(QString::fromLatin1(kGatewayProbePath), QJsonObject {}, responseBody); + if (err != ErrorCode::NoError) { + emit errorOccurred(err); + return false; + } + return true; +} + void PairingUiController::resetTvQrDisplay() { m_tvQrCodes.clear(); diff --git a/client/ui/controllers/api/pairingUiController.h b/client/ui/controllers/api/pairingUiController.h index 0df513fb6..778a26650 100644 --- a/client/ui/controllers/api/pairingUiController.h +++ b/client/ui/controllers/api/pairingUiController.h @@ -61,6 +61,8 @@ public: #endif public slots: + /** Fast preflight before opening receive QR page; emits errorOccurred on failure. */ + bool canOpenTvQrPairingPage(); void startTvQrSession(); void cancelTvQrSession(); /** TV receive + phone send: call when leaving QR pairing (back / pop) so long-poll state does not stick. */ diff --git a/client/ui/qml/Pages2/PageSettingsApiDevices.qml b/client/ui/qml/Pages2/PageSettingsApiDevices.qml index 54ccd7644..365adb9ce 100644 --- a/client/ui/qml/Pages2/PageSettingsApiDevices.qml +++ b/client/ui/qml/Pages2/PageSettingsApiDevices.qml @@ -33,12 +33,15 @@ PageType { target: PairingUiController function onPhonePairingSucceeded() { + const serverIndex = ServersUiController.getProcessedServerIndex() + if (serverIndex < 0) { + return + } + SubscriptionUiController.getAccountInfo(serverIndex, true) + SubscriptionUiController.updateApiDevicesModel() if (!root.visible) { return } - const serverIndex = ServersUiController.getProcessedServerIndex() - SubscriptionUiController.getAccountInfo(serverIndex, true) - SubscriptionUiController.updateApiDevicesModel() const label = PairingUiController.lastSuccessfulPhonePairingDisplayName if (label.length > 0) { PageController.showNotificationMessage( diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 74f12e6bc..883b14ec6 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -353,7 +353,12 @@ PageType { property string imageSource: "qrc:/images/controls/folder-search-2.svg" property bool isVisible: true property var handler: function() { - PageController.goToPage(PageEnum.PageSetupWizardApiQrPairingReceive) + PageController.showBusyIndicator(true) + var result = PairingUiController.canOpenTvQrPairingPage() + PageController.showBusyIndicator(false) + if (result) { + PageController.goToPage(PageEnum.PageSetupWizardApiQrPairingReceive) + } } } diff --git a/tools/local_gateway/main.go b/tools/local_gateway/main.go index f38f0d253..ab2e24009 100644 --- a/tools/local_gateway/main.go +++ b/tools/local_gateway/main.go @@ -32,6 +32,7 @@ var ( mu sync.Mutex requests = map[string][]time.Time{} // installation_uuid -> timestamps (sliding window simplified: count in session) sessions = map[string]*pairingSession{} + issued = map[string]issuedConfigInfo{} // installation_uuid -> issued config info shown in /v1/account_info // Configured from flags / env in main(). pairingSessionTTL = 30 * time.Second @@ -70,11 +71,31 @@ type pairingResult struct { } type pairingSession struct { - QRUUID string - ExpiresAt time.Time - Done chan struct{} - Result *pairingResult - Completed bool + QRUUID string + DesktopInstallUUID string + DesktopOSVersion string + ExpiresAt time.Time + Done chan struct{} + Result *pairingResult + Completed bool +} + +type issuedConfigInfo struct { + InstallationUUID string `json:"installation_uuid"` + WorkerLastUpdated string `json:"worker_last_updated"` + LastDownloaded string `json:"last_downloaded"` + SourceType string `json:"source_type"` + OSVersion string `json:"os_version"` + ServerCountryCode string `json:"server_country_code"` + ServerCountryName string `json:"server_country_name"` +} + +func stringFromMap(m map[string]any, key string) string { + if m == nil { + return "" + } + v, _ := m[key].(string) + return strings.TrimSpace(v) } func writeJSON(w http.ResponseWriter, status int, body any) { @@ -184,9 +205,11 @@ func handleGenerateQR(w http.ResponseWriter, r *http.Request) { } session := &pairingSession{ - QRUUID: req.QRUUID, - ExpiresAt: time.Now().Add(pairingSessionTTL), - Done: make(chan struct{}), + QRUUID: req.QRUUID, + DesktopInstallUUID: req.InstallationUUID, + DesktopOSVersion: req.OSVersion, + ExpiresAt: time.Now().Add(pairingSessionTTL), + Done: make(chan struct{}), } mu.Lock() @@ -276,12 +299,44 @@ func handleScanQR(w http.ResponseWriter, r *http.Request) { ServiceInfo: req.ServiceInfo, SupportedProto: req.SupportedProto, } + nowISO := time.Now().UTC().Format(time.RFC3339) + countryCode := stringFromMap(req.ServiceInfo, "server_country_code") + if countryCode == "" { + countryCode = stringFromMap(req.ServiceInfo, "country_code") + } + if countryCode == "" { + countryCode = "ZZ" + } + countryName := stringFromMap(req.ServiceInfo, "server_country_name") + if countryName == "" { + countryName = stringFromMap(req.ServiceInfo, "country_name") + } + if countryName == "" { + countryName = "Mock Country" + } + desktopUUID := strings.TrimSpace(session.DesktopInstallUUID) + if desktopUUID == "" { + desktopUUID = strings.TrimSpace(req.InstallationUUID) + } + desktopOS := strings.TrimSpace(session.DesktopOSVersion) + if desktopOS == "" { + desktopOS = strings.TrimSpace(req.OSVersion) + } + issued[desktopUUID] = issuedConfigInfo{ + InstallationUUID: desktopUUID, + WorkerLastUpdated: nowISO, + LastDownloaded: nowISO, + SourceType: "gateway_account", + OSVersion: desktopOS, + ServerCountryCode: countryCode, + ServerCountryName: countryName, + } session.Completed = true close(session.Done) mu.Unlock() - log.Printf("pairing COMPLETED uuid=%s phone_install=%s config_len=%d proto_count=%d", - shortID(req.QRUUID), shortID(req.InstallationUUID), len(req.Config), len(req.SupportedProto)) + log.Printf("pairing COMPLETED uuid=%s phone_install=%s desktop_install=%s config_len=%d proto_count=%d", + shortID(req.QRUUID), shortID(req.InstallationUUID), shortID(desktopUUID), len(req.Config), len(req.SupportedProto)) writeJSON(w, http.StatusOK, map[string]string{"message": "OK"}) } @@ -442,17 +497,27 @@ func handleAccountInfo(w http.ResponseWriter, r *http.Request) { } drainBody(r) + mu.Lock() + issuedConfigs := make([]issuedConfigInfo, 0, len(issued)) + for _, cfg := range issued { + issuedConfigs = append(issuedConfigs, cfg) + } + mu.Unlock() + sort.Slice(issuedConfigs, func(i, j int) bool { + return issuedConfigs[i].InstallationUUID < issuedConfigs[j].InstallationUUID + }) + // Keys match client/core/utils/constants/apiKeys.h (snake_case). endDate := time.Now().UTC().AddDate(1, 0, 0).Format(time.RFC3339) resp := map[string]any{ - "active_device_count": 1, + "active_device_count": len(issuedConfigs), "max_device_count": 5, "subscription_end_date": endDate, "subscription_description": "Local mock (tools/local_gateway)", "is_renewal_available": false, "supported_protocols": []string{"awg", "vless"}, "available_countries": []any{}, - "issued_configs": []any{}, + "issued_configs": issuedConfigs, "support_info": map[string]any{ "telegram": "amnezia_support", "email": "support@example.com",