From ad8634778bcd81a0c649e958a4aeab72dfdc2da3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 21:44:25 +0000 Subject: [PATCH 1/5] Initial plan From 92fa7596b0e1a4d0416378ee404ffbcd10240d00 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 21:54:11 +0000 Subject: [PATCH 2/5] Fix open redirect vulnerability in action-details page with URL allowlist validation Agent-Logs-Url: https://github.com/numbersprotocol/capture-cam/sessions/d25025b1-af37-4689-84c5-a319e266346e Co-authored-by: numbers-official <181934381+numbers-official@users.noreply.github.com> --- .../action-details.page.spec.ts | 105 +++++++++++++++++- .../action-details/action-details.page.ts | 22 ++++ src/assets/i18n/en-us.json | 1 + src/assets/i18n/zh-tw.json | 1 + 4 files changed, 128 insertions(+), 1 deletion(-) diff --git a/src/app/features/home/details/actions/action-details/action-details.page.spec.ts b/src/app/features/home/details/actions/action-details/action-details.page.spec.ts index 2fe0c81dc..043b2e819 100644 --- a/src/app/features/home/details/actions/action-details/action-details.page.spec.ts +++ b/src/app/features/home/details/actions/action-details/action-details.page.spec.ts @@ -1,24 +1,127 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; - +import { ActivatedRoute, convertToParamMap } from '@angular/router'; +import { Browser } from '@capacitor/browser'; +import { EMPTY, of } from 'rxjs'; +import { ErrorService } from '../../../../../shared/error/error.service'; import { SharedTestingModule } from '../../../../../shared/shared-testing.module'; import { ActionDetailsPage } from './action-details.page'; describe('ActionDetailsPage', () => { let component: ActionDetailsPage; let fixture: ComponentFixture; + let errorService: ErrorService; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ActionDetailsPage], imports: [SharedTestingModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + paramMap: of(convertToParamMap({ id: 'test-cid' })), + }, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(ActionDetailsPage); component = fixture.componentInstance; + errorService = TestBed.inject(ErrorService); fixture.detectChanges(); })); it('should create', () => { expect(component).toBeTruthy(); }); + + describe('redirectToExternalUrl', () => { + let browserOpenSpy: jasmine.Spy; + let toastErrorSpy: jasmine.Spy; + + beforeEach(() => { + browserOpenSpy = spyOn(Browser, 'open').and.resolveTo(); + toastErrorSpy = spyOn(errorService, 'toastError$').and.returnValue(EMPTY); + }); + + it('should open valid HTTPS URL from allowlisted domain captureapp.xyz', () => { + component.redirectToExternalUrl( + 'https://app.captureapp.xyz/action', + 'order-1' + ); + expect(browserOpenSpy).toHaveBeenCalledWith( + jasmine.objectContaining({ + url: jasmine.stringContaining('https://app.captureapp.xyz/action'), + }) + ); + expect(toastErrorSpy).not.toHaveBeenCalled(); + }); + + it('should open valid HTTPS URL from allowlisted domain numbersprotocol.io', () => { + component.redirectToExternalUrl( + 'https://numbersprotocol.io/action', + 'order-2' + ); + expect(browserOpenSpy).toHaveBeenCalledWith( + jasmine.objectContaining({ + url: jasmine.stringContaining('https://numbersprotocol.io/action'), + }) + ); + expect(toastErrorSpy).not.toHaveBeenCalled(); + }); + + it('should open valid HTTPS URL from legitimate subdomain of allowlisted domain', () => { + component.redirectToExternalUrl( + 'https://subdomain.captureapp.xyz/action', + 'order-7' + ); + expect(browserOpenSpy).toHaveBeenCalledWith( + jasmine.objectContaining({ + url: jasmine.stringContaining( + 'https://subdomain.captureapp.xyz/action' + ), + }) + ); + expect(toastErrorSpy).not.toHaveBeenCalled(); + }); + + it('should reject HTTP URLs', () => { + component.redirectToExternalUrl( + 'http://captureapp.xyz/action', + 'order-3' + ); + expect(browserOpenSpy).not.toHaveBeenCalled(); + expect(toastErrorSpy).toHaveBeenCalled(); + }); + + it('should reject URLs from non-allowlisted domains', () => { + component.redirectToExternalUrl('https://evil.example.com', 'order-4'); + expect(browserOpenSpy).not.toHaveBeenCalled(); + expect(toastErrorSpy).toHaveBeenCalled(); + }); + + it('should reject URLs that try to spoof allowlisted domains via subdomain confusion', () => { + component.redirectToExternalUrl( + 'https://captureapp.xyz.evil.com', + 'order-5' + ); + expect(browserOpenSpy).not.toHaveBeenCalled(); + expect(toastErrorSpy).toHaveBeenCalled(); + }); + + it('should reject URLs that try to spoof allowlisted domains via similar domain names', () => { + component.redirectToExternalUrl( + 'https://evil-captureapp.xyz', + 'order-8' + ); + expect(browserOpenSpy).not.toHaveBeenCalled(); + expect(toastErrorSpy).toHaveBeenCalled(); + }); + + it('should reject invalid URLs', () => { + component.redirectToExternalUrl('not-a-url', 'order-6'); + expect(browserOpenSpy).not.toHaveBeenCalled(); + expect(toastErrorSpy).toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/features/home/details/actions/action-details/action-details.page.ts b/src/app/features/home/details/actions/action-details/action-details.page.ts index 582c9762a..111a343b5 100644 --- a/src/app/features/home/details/actions/action-details/action-details.page.ts +++ b/src/app/features/home/details/actions/action-details/action-details.page.ts @@ -57,6 +57,8 @@ import { InformationSessionService } from '../../information/session/information styleUrls: ['./action-details.page.scss'], }) export class ActionDetailsPage { + private readonly ALLOWED_DOMAINS = ['captureapp.xyz', 'numbersprotocol.io']; + readonly id$ = this.route.paramMap.pipe( map(params => params.get('id')), isNonNullable() @@ -371,7 +373,27 @@ export class ActionDetailsPage { ); } + private isValidRedirectUrl(url: string): boolean { + try { + const parsed = new URL(url); + if (parsed.protocol !== 'https:') return false; + return this.ALLOWED_DOMAINS.some( + d => parsed.hostname === d || parsed.hostname.endsWith(`.${d}`) + ); + } catch { + return false; + } + } + redirectToExternalUrl(url: string, orderId: string) { + if (!this.isValidRedirectUrl(url)) { + this.errorService + .toastError$( + this.translocoService.translate('error.invalidRedirectUrl') + ) + .subscribe(); + return; + } this.id$ .pipe( first(), diff --git a/src/assets/i18n/en-us.json b/src/assets/i18n/en-us.json index 72ed4e25c..c1a3e2f86 100644 --- a/src/assets/i18n/en-us.json +++ b/src/assets/i18n/en-us.json @@ -196,6 +196,7 @@ "internetError": "Internet connection error. Please check your Internet connection quality.", "timeoutError": "Operation timeout. Please try again later.", "unknownError": "An error has occurred. Please try again later.", + "invalidRedirectUrl": "Invalid redirect URL.", "geolocation": { "permissionDeniedError": "Location permission is denied. Please turn your location permission on to enable the full feature of Capture app.", "timeoutError": "Location collection timeout, possibly due to weak network connectivity or GPS signals.", diff --git a/src/assets/i18n/zh-tw.json b/src/assets/i18n/zh-tw.json index 463ca6456..0125f22ea 100644 --- a/src/assets/i18n/zh-tw.json +++ b/src/assets/i18n/zh-tw.json @@ -196,6 +196,7 @@ "internetError": "網路連線錯誤,請確認您的連線品質是否良好。", "timeoutError": "操作逾時,請稍後再試。", "unknownError": "錯誤發生,請稍後再試。", + "invalidRedirectUrl": "無效的重定向網址。", "geolocation": { "permissionDeniedError": "位置存取被拒絕。如欲使用 Capture App 的完整功能,請開啟位置權限。", "timeoutError": "取得位置資訊花費過長。可能原因為網路連線或 GPS 信號不穩。", From 7abf4990173c808c1adf40cddf2987f23f66fef0 Mon Sep 17 00:00:00 2001 From: Omni Date: Thu, 16 Apr 2026 06:51:52 +0000 Subject: [PATCH 3/5] fix: use fakeAsync+tick in positive redirect tests for async observable The redirectToExternalUrl method subscribes to id$ observable internally. Positive tests need fakeAsync+tick to flush the observable before asserting Browser.open was called. Also fixes Prettier formatting. --- .../action-details.page.spec.ts | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/app/features/home/details/actions/action-details/action-details.page.spec.ts b/src/app/features/home/details/actions/action-details/action-details.page.spec.ts index 043b2e819..a1a46a38a 100644 --- a/src/app/features/home/details/actions/action-details/action-details.page.spec.ts +++ b/src/app/features/home/details/actions/action-details/action-details.page.spec.ts @@ -1,4 +1,10 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; import { ActivatedRoute, convertToParamMap } from '@angular/router'; import { Browser } from '@capacitor/browser'; import { EMPTY, of } from 'rxjs'; @@ -44,37 +50,40 @@ describe('ActionDetailsPage', () => { toastErrorSpy = spyOn(errorService, 'toastError$').and.returnValue(EMPTY); }); - it('should open valid HTTPS URL from allowlisted domain captureapp.xyz', () => { + it('should open valid HTTPS URL from allowlisted domain captureapp.xyz', fakeAsync(() => { component.redirectToExternalUrl( 'https://app.captureapp.xyz/action', 'order-1' ); + tick(); expect(browserOpenSpy).toHaveBeenCalledWith( jasmine.objectContaining({ url: jasmine.stringContaining('https://app.captureapp.xyz/action'), }) ); expect(toastErrorSpy).not.toHaveBeenCalled(); - }); + })); - it('should open valid HTTPS URL from allowlisted domain numbersprotocol.io', () => { + it('should open valid HTTPS URL from allowlisted domain numbersprotocol.io', fakeAsync(() => { component.redirectToExternalUrl( 'https://numbersprotocol.io/action', 'order-2' ); + tick(); expect(browserOpenSpy).toHaveBeenCalledWith( jasmine.objectContaining({ url: jasmine.stringContaining('https://numbersprotocol.io/action'), }) ); expect(toastErrorSpy).not.toHaveBeenCalled(); - }); + })); - it('should open valid HTTPS URL from legitimate subdomain of allowlisted domain', () => { + it('should open valid HTTPS URL from legitimate subdomain of allowlisted domain', fakeAsync(() => { component.redirectToExternalUrl( 'https://subdomain.captureapp.xyz/action', 'order-7' ); + tick(); expect(browserOpenSpy).toHaveBeenCalledWith( jasmine.objectContaining({ url: jasmine.stringContaining( @@ -83,7 +92,7 @@ describe('ActionDetailsPage', () => { }) ); expect(toastErrorSpy).not.toHaveBeenCalled(); - }); + })); it('should reject HTTP URLs', () => { component.redirectToExternalUrl( @@ -110,10 +119,7 @@ describe('ActionDetailsPage', () => { }); it('should reject URLs that try to spoof allowlisted domains via similar domain names', () => { - component.redirectToExternalUrl( - 'https://evil-captureapp.xyz', - 'order-8' - ); + component.redirectToExternalUrl('https://evil-captureapp.xyz', 'order-8'); expect(browserOpenSpy).not.toHaveBeenCalled(); expect(toastErrorSpy).toHaveBeenCalled(); }); From adba4a12f0c4ea8457be57130d96f7d614db53f6 Mon Sep 17 00:00:00 2001 From: Omni Date: Thu, 16 Apr 2026 07:09:13 +0000 Subject: [PATCH 4/5] test: override id$ and remove fakeAsync from action-details tests by Olga Shen & Omni --- .../action-details.page.spec.ts | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/app/features/home/details/actions/action-details/action-details.page.spec.ts b/src/app/features/home/details/actions/action-details/action-details.page.spec.ts index a1a46a38a..b020ff14f 100644 --- a/src/app/features/home/details/actions/action-details/action-details.page.spec.ts +++ b/src/app/features/home/details/actions/action-details/action-details.page.spec.ts @@ -1,10 +1,4 @@ -import { - ComponentFixture, - fakeAsync, - TestBed, - tick, - waitForAsync, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ActivatedRoute, convertToParamMap } from '@angular/router'; import { Browser } from '@capacitor/browser'; import { EMPTY, of } from 'rxjs'; @@ -46,44 +40,49 @@ describe('ActionDetailsPage', () => { let toastErrorSpy: jasmine.Spy; beforeEach(() => { + // Override id$ to bypass ActivatedRoute/RouterTestingModule dependency. + // RouterTestingModule may provide its own ActivatedRoute that shadows + // the test mock, causing id$ to never emit. + Object.defineProperty(component, 'id$', { + value: of('test-cid'), + writable: true, + configurable: true, + }); browserOpenSpy = spyOn(Browser, 'open').and.resolveTo(); toastErrorSpy = spyOn(errorService, 'toastError$').and.returnValue(EMPTY); }); - it('should open valid HTTPS URL from allowlisted domain captureapp.xyz', fakeAsync(() => { + it('should open valid HTTPS URL from allowlisted domain captureapp.xyz', () => { component.redirectToExternalUrl( 'https://app.captureapp.xyz/action', 'order-1' ); - tick(); expect(browserOpenSpy).toHaveBeenCalledWith( jasmine.objectContaining({ url: jasmine.stringContaining('https://app.captureapp.xyz/action'), }) ); expect(toastErrorSpy).not.toHaveBeenCalled(); - })); + }); - it('should open valid HTTPS URL from allowlisted domain numbersprotocol.io', fakeAsync(() => { + it('should open valid HTTPS URL from allowlisted domain numbersprotocol.io', () => { component.redirectToExternalUrl( 'https://numbersprotocol.io/action', 'order-2' ); - tick(); expect(browserOpenSpy).toHaveBeenCalledWith( jasmine.objectContaining({ url: jasmine.stringContaining('https://numbersprotocol.io/action'), }) ); expect(toastErrorSpy).not.toHaveBeenCalled(); - })); + }); - it('should open valid HTTPS URL from legitimate subdomain of allowlisted domain', fakeAsync(() => { + it('should open valid HTTPS URL from legitimate subdomain of allowlisted domain', () => { component.redirectToExternalUrl( 'https://subdomain.captureapp.xyz/action', 'order-7' ); - tick(); expect(browserOpenSpy).toHaveBeenCalledWith( jasmine.objectContaining({ url: jasmine.stringContaining( @@ -92,7 +91,7 @@ describe('ActionDetailsPage', () => { }) ); expect(toastErrorSpy).not.toHaveBeenCalled(); - })); + }); it('should reject HTTP URLs', () => { component.redirectToExternalUrl( From 7e858279cbdbd4d32a485b91c51ce073d4d774e7 Mon Sep 17 00:00:00 2001 From: Omni Date: Thu, 16 Apr 2026 07:37:21 +0000 Subject: [PATCH 5/5] refactor: remove redirectToExternalUrl feature by Olga Shen & Omni --- .../action-details.page.spec.ts | 110 +----------------- .../action-details/action-details.page.ts | 51 -------- .../shared/actions/service/actions.service.ts | 1 - src/assets/i18n/en-us.json | 1 - src/assets/i18n/zh-tw.json | 1 - 5 files changed, 1 insertion(+), 163 deletions(-) diff --git a/src/app/features/home/details/actions/action-details/action-details.page.spec.ts b/src/app/features/home/details/actions/action-details/action-details.page.spec.ts index b020ff14f..2fe0c81dc 100644 --- a/src/app/features/home/details/actions/action-details/action-details.page.spec.ts +++ b/src/app/features/home/details/actions/action-details/action-details.page.spec.ts @@ -1,132 +1,24 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ActivatedRoute, convertToParamMap } from '@angular/router'; -import { Browser } from '@capacitor/browser'; -import { EMPTY, of } from 'rxjs'; -import { ErrorService } from '../../../../../shared/error/error.service'; + import { SharedTestingModule } from '../../../../../shared/shared-testing.module'; import { ActionDetailsPage } from './action-details.page'; describe('ActionDetailsPage', () => { let component: ActionDetailsPage; let fixture: ComponentFixture; - let errorService: ErrorService; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ActionDetailsPage], imports: [SharedTestingModule], - providers: [ - { - provide: ActivatedRoute, - useValue: { - paramMap: of(convertToParamMap({ id: 'test-cid' })), - }, - }, - ], }).compileComponents(); fixture = TestBed.createComponent(ActionDetailsPage); component = fixture.componentInstance; - errorService = TestBed.inject(ErrorService); fixture.detectChanges(); })); it('should create', () => { expect(component).toBeTruthy(); }); - - describe('redirectToExternalUrl', () => { - let browserOpenSpy: jasmine.Spy; - let toastErrorSpy: jasmine.Spy; - - beforeEach(() => { - // Override id$ to bypass ActivatedRoute/RouterTestingModule dependency. - // RouterTestingModule may provide its own ActivatedRoute that shadows - // the test mock, causing id$ to never emit. - Object.defineProperty(component, 'id$', { - value: of('test-cid'), - writable: true, - configurable: true, - }); - browserOpenSpy = spyOn(Browser, 'open').and.resolveTo(); - toastErrorSpy = spyOn(errorService, 'toastError$').and.returnValue(EMPTY); - }); - - it('should open valid HTTPS URL from allowlisted domain captureapp.xyz', () => { - component.redirectToExternalUrl( - 'https://app.captureapp.xyz/action', - 'order-1' - ); - expect(browserOpenSpy).toHaveBeenCalledWith( - jasmine.objectContaining({ - url: jasmine.stringContaining('https://app.captureapp.xyz/action'), - }) - ); - expect(toastErrorSpy).not.toHaveBeenCalled(); - }); - - it('should open valid HTTPS URL from allowlisted domain numbersprotocol.io', () => { - component.redirectToExternalUrl( - 'https://numbersprotocol.io/action', - 'order-2' - ); - expect(browserOpenSpy).toHaveBeenCalledWith( - jasmine.objectContaining({ - url: jasmine.stringContaining('https://numbersprotocol.io/action'), - }) - ); - expect(toastErrorSpy).not.toHaveBeenCalled(); - }); - - it('should open valid HTTPS URL from legitimate subdomain of allowlisted domain', () => { - component.redirectToExternalUrl( - 'https://subdomain.captureapp.xyz/action', - 'order-7' - ); - expect(browserOpenSpy).toHaveBeenCalledWith( - jasmine.objectContaining({ - url: jasmine.stringContaining( - 'https://subdomain.captureapp.xyz/action' - ), - }) - ); - expect(toastErrorSpy).not.toHaveBeenCalled(); - }); - - it('should reject HTTP URLs', () => { - component.redirectToExternalUrl( - 'http://captureapp.xyz/action', - 'order-3' - ); - expect(browserOpenSpy).not.toHaveBeenCalled(); - expect(toastErrorSpy).toHaveBeenCalled(); - }); - - it('should reject URLs from non-allowlisted domains', () => { - component.redirectToExternalUrl('https://evil.example.com', 'order-4'); - expect(browserOpenSpy).not.toHaveBeenCalled(); - expect(toastErrorSpy).toHaveBeenCalled(); - }); - - it('should reject URLs that try to spoof allowlisted domains via subdomain confusion', () => { - component.redirectToExternalUrl( - 'https://captureapp.xyz.evil.com', - 'order-5' - ); - expect(browserOpenSpy).not.toHaveBeenCalled(); - expect(toastErrorSpy).toHaveBeenCalled(); - }); - - it('should reject URLs that try to spoof allowlisted domains via similar domain names', () => { - component.redirectToExternalUrl('https://evil-captureapp.xyz', 'order-8'); - expect(browserOpenSpy).not.toHaveBeenCalled(); - expect(toastErrorSpy).toHaveBeenCalled(); - }); - - it('should reject invalid URLs', () => { - component.redirectToExternalUrl('not-a-url', 'order-6'); - expect(browserOpenSpy).not.toHaveBeenCalled(); - expect(toastErrorSpy).toHaveBeenCalled(); - }); - }); }); diff --git a/src/app/features/home/details/actions/action-details/action-details.page.ts b/src/app/features/home/details/actions/action-details/action-details.page.ts index 111a343b5..5dab7ef61 100644 --- a/src/app/features/home/details/actions/action-details/action-details.page.ts +++ b/src/app/features/home/details/actions/action-details/action-details.page.ts @@ -3,7 +3,6 @@ import { UntypedFormGroup } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; import { ActivatedRoute, Router } from '@angular/router'; -import { Browser } from '@capacitor/browser'; import { NavController } from '@ionic/angular'; import { TranslocoService } from '@jsverse/transloco'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; @@ -43,7 +42,6 @@ import { DiaBackendWalletService } from '../../../../../shared/dia-backend/walle import { ErrorService } from '../../../../../shared/error/error.service'; import { OrderDetailDialogComponent } from '../../../../../shared/order-detail-dialog/order-detail-dialog.component'; import { ProofRepository } from '../../../../../shared/repositories/proof/proof-repository.service'; -import { browserToolbarColor } from '../../../../../utils/constants'; import { VOID$, isNonNullable, @@ -57,8 +55,6 @@ import { InformationSessionService } from '../../information/session/information styleUrls: ['./action-details.page.scss'], }) export class ActionDetailsPage { - private readonly ALLOWED_DOMAINS = ['captureapp.xyz', 'numbersprotocol.io']; - readonly id$ = this.route.paramMap.pipe( map(params => params.get('id')), isNonNullable() @@ -327,14 +323,6 @@ export class ActionDetailsPage { this.navController.back(); } }), - tap(networkAppOrder => { - if (action.ext_action_destination_text) { - this.redirectToExternalUrl( - action.ext_action_destination_text, - networkAppOrder.id - ); - } - }), untilDestroyed(this) ); }) @@ -373,45 +361,6 @@ export class ActionDetailsPage { ); } - private isValidRedirectUrl(url: string): boolean { - try { - const parsed = new URL(url); - if (parsed.protocol !== 'https:') return false; - return this.ALLOWED_DOMAINS.some( - d => parsed.hostname === d || parsed.hostname.endsWith(`.${d}`) - ); - } catch { - return false; - } - } - - redirectToExternalUrl(url: string, orderId: string) { - if (!this.isValidRedirectUrl(url)) { - this.errorService - .toastError$( - this.translocoService.translate('error.invalidRedirectUrl') - ) - .subscribe(); - return; - } - this.id$ - .pipe( - first(), - isNonNullable(), - tap(cid => - Browser.open({ - url: `${url}?cid=${cid}&order_id=${orderId}`, - toolbarColor: browserToolbarColor, - }) - ), - catchError((err: unknown) => { - return this.errorService.toastError$(err); - }), - untilDestroyed(this) - ) - .subscribe(); - } - removeCaptureAndNavigateHome() { if (this.informationSessionService.activatedDetailedCapture) { this.informationSessionService.activatedDetailedCapture.proof$.subscribe( diff --git a/src/app/shared/actions/service/actions.service.ts b/src/app/shared/actions/service/actions.service.ts index 97f3548aa..fca448069 100644 --- a/src/app/shared/actions/service/actions.service.ts +++ b/src/app/shared/actions/service/actions.service.ts @@ -48,7 +48,6 @@ export interface Action { readonly params_list_custom_param1?: string[]; readonly title_text: string; readonly network_app_id_text: string; - readonly ext_action_destination_text?: string; readonly hide_capture_after_execution_boolean?: boolean; } diff --git a/src/assets/i18n/en-us.json b/src/assets/i18n/en-us.json index c1a3e2f86..72ed4e25c 100644 --- a/src/assets/i18n/en-us.json +++ b/src/assets/i18n/en-us.json @@ -196,7 +196,6 @@ "internetError": "Internet connection error. Please check your Internet connection quality.", "timeoutError": "Operation timeout. Please try again later.", "unknownError": "An error has occurred. Please try again later.", - "invalidRedirectUrl": "Invalid redirect URL.", "geolocation": { "permissionDeniedError": "Location permission is denied. Please turn your location permission on to enable the full feature of Capture app.", "timeoutError": "Location collection timeout, possibly due to weak network connectivity or GPS signals.", diff --git a/src/assets/i18n/zh-tw.json b/src/assets/i18n/zh-tw.json index 0125f22ea..463ca6456 100644 --- a/src/assets/i18n/zh-tw.json +++ b/src/assets/i18n/zh-tw.json @@ -196,7 +196,6 @@ "internetError": "網路連線錯誤,請確認您的連線品質是否良好。", "timeoutError": "操作逾時,請稍後再試。", "unknownError": "錯誤發生,請稍後再試。", - "invalidRedirectUrl": "無效的重定向網址。", "geolocation": { "permissionDeniedError": "位置存取被拒絕。如欲使用 Capture App 的完整功能,請開啟位置權限。", "timeoutError": "取得位置資訊花費過長。可能原因為網路連線或 GPS 信號不穩。",