mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-20 02:00:55 +07:00
fixed open QR code screen & fix iOS scanner
This commit is contained in:
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user