fixed open QR code screen & fix iOS scanner

This commit is contained in:
dranik
2026-05-08 10:21:24 +03:00
parent 433ecb448f
commit a53db6eafe
6 changed files with 129 additions and 21 deletions
+16 -5
View File
@@ -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) {
@@ -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();
@@ -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. */
@@ -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(
@@ -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)
}
}
}
+77 -12
View File
@@ -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",