Skip to content

Commit b54bd4a

Browse files
committed
fix: handle private validation URL redirect and string error messages
When make_validation_url_private=true, the backend returns HTTP 401 with action=REDIRECT and errors as a string array. The frontend was only handling object-format errors ({message}) and not the redirect action, causing a false 'Failed to validate document' message. Adds handleValidationRedirect() and fixes getValidationErrorMessage() to handle both string and object error formats. Signed-off-by: Vitor Mattos <vitor@php.rio> Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 430496b commit b54bd4a

4 files changed

Lines changed: 112 additions & 4 deletions

File tree

src/components/PdfEditor/PdfEditor.vue

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
:init-file-names="fileNames"
1010
:page-count-format="t('libresign', '{currentPage} of {totalPages}')"
1111
:page-aria-label="getPageAriaLabel"
12-
:auto-fit-zoom="true"
12+
:auto-fit-zoom="enableAutoFitZoom"
1313
:read-only="readOnly"
1414
:emit-object-click="true"
1515
:hide-selection-ui="readOnly"
@@ -129,6 +129,9 @@ type PdfElementsInstance = {
129129
pdfDocuments?: PdfDocument[]
130130
selectedDocIndex?: number
131131
autoFitZoom?: boolean
132+
scale?: number
133+
visualScale?: number
134+
commitZoom?: () => void
132135
}
133136
134137
defineOptions({
@@ -158,6 +161,17 @@ const emit = defineEmits<{
158161
159162
const pdfElements = ref<PdfElementsInstance | null>(null)
160163
164+
// Auto-fit can fight user zoom on touch devices; keep one-shot fit from endInit and let user control zoom afterwards.
165+
const enableAutoFitZoom = computed(() => {
166+
const isTouchDevice = typeof window !== 'undefined'
167+
&& (
168+
(window.matchMedia?.('(pointer: coarse)').matches ?? false)
169+
|| 'ontouchstart' in window
170+
|| (typeof navigator !== 'undefined' && navigator.maxTouchPoints > 0)
171+
)
172+
return !isTouchDevice
173+
})
174+
161175
const ignoreClickOutsideSelectors = computed(() => ['.action-item__popper', '.action-item'])
162176
163177
const toolbarStyleVars = computed(() => ({
@@ -313,6 +327,8 @@ function cancelAdding() {
313327
pdfElements.value?.cancelAdding()
314328
}
315329
330+
331+
316332
async function addSigner(signer: SignerSummaryRecord | SignerDetailRecord, visibleElement: VisibleElementRecord, options: { documentIndex?: number } = {}) {
317333
if (!pdfElements.value || !visibleElement.coordinates) {
318334
return
@@ -385,6 +401,7 @@ defineExpose({
385401
findObjectLocation,
386402
startAddingSigner,
387403
cancelAdding,
404+
388405
addSigner,
389406
waitForPageRender,
390407
getTotalObjectsCount,
@@ -396,6 +413,8 @@ defineExpose({
396413
.pdf-editor {
397414
width: 100%;
398415
height: 100%;
416+
overflow: hidden;
417+
overscroll-behavior: contain;
399418
}
400419
401420
</style>

src/tests/views/SignPDF/Sign.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1687,6 +1687,54 @@ describe('Sign.vue - signWithTokenCode', () => {
16871687
expect(wrapper.vm.hasSignatures).toBe(false)
16881688
expect(wrapper.vm.needCreateSignature).toBe(false)
16891689
})
1690+
1691+
it('shows a mobile orientation hint when signature setup is required on portrait phones', async () => {
1692+
const { default: realSign } = await import('../../../views/SignPDF/_partials/Sign.vue')
1693+
const { useSignStore } = await import('../../../store/sign.js')
1694+
const signStore = useSignStore()
1695+
1696+
signStore.document = createSignDocument({
1697+
nodeType: 'file',
1698+
signers: [
1699+
{ signRequestId: 501, me: true },
1700+
],
1701+
visibleElements: [
1702+
{ elementId: 201, fileId: 1, signRequestId: 501, type: 'signature', coordinates: { page: 1, left: 10, top: 20, width: 30, height: 40 } },
1703+
],
1704+
})
1705+
1706+
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 390, writable: true })
1707+
Object.defineProperty(window, 'innerHeight', { configurable: true, value: 844, writable: true })
1708+
1709+
const wrapper = mount(realSign, {
1710+
global: {
1711+
stubs: {
1712+
NcButton: true,
1713+
NcDialog: true,
1714+
NcLoadingIcon: true,
1715+
TokenManager: true,
1716+
EmailManager: true,
1717+
UploadCertificate: true,
1718+
Documents: true,
1719+
Signatures: true,
1720+
Draw: true,
1721+
ManagePassword: true,
1722+
CreatePassword: true,
1723+
NcNoteCard: false,
1724+
NcPasswordField: true,
1725+
NcRichText: true,
1726+
},
1727+
mocks: {
1728+
$watch: vi.fn(),
1729+
},
1730+
},
1731+
})
1732+
1733+
await flushPromises()
1734+
1735+
expect(wrapper.vm.needCreateSignature).toBe(true)
1736+
expect(wrapper.text()).toContain('For a better signing experience on mobile, rotate your phone to landscape mode.')
1737+
})
16901738
})
16911739

16921740
describe('Sign.vue - create signature modal', () => {

src/views/SignPDF/_partials/Sign.vue

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
<Signatures v-if="hasSignatures" />
99
</div>
1010
<div v-if="!loading" class="button-wrapper">
11+
<NcNoteCard v-if="showMobileOrientationHint" type="warning">
12+
{{ t('libresign', 'For a better signing experience on mobile, rotate your phone to landscape mode.') }}
13+
</NcNoteCard>
1114
<NcNoteCard v-for="(error, index) in signStore.errors"
1215
:key="index"
1316
:heading="error.title || ''"
@@ -433,6 +436,7 @@ const sidebarStore = useSidebarStore() as SidebarStoreContract
433436
const identificationDocumentStore = useIdentificationDocumentStore() as IdentificationDocumentStoreContract
434437
435438
const loading = ref(true)
439+
const isMobilePortrait = ref(false)
436440
const user = ref<UserInfo>({
437441
account: { uid: '', emailAddress: '', displayName: '' },
438442
settings: { canRequestSign: false, hasSignatureFile: false, phoneNumber: '' },
@@ -472,6 +476,7 @@ const needCreateSignature = computed(() => {
472476
}
473477
return hasVisibleElementsForCurrentUser(visibleElementsDocument.value)
474478
})
479+
const showMobileOrientationHint = computed(() => needCreateSignature.value && isMobilePortrait.value)
475480
const needIdentificationDocuments = computed(() => identificationDocumentStore.showDocumentsComponent())
476481
const canCreateSignature = computed(() => {
477482
const capabilities = getCapabilities() as LibresignCapabilities
@@ -543,6 +548,19 @@ function clearBlockingSignError() {
543548
signStore.clearSigningErrors()
544549
}
545550
551+
function updateOrientationHint() {
552+
if (typeof window === 'undefined') {
553+
isMobilePortrait.value = false
554+
return
555+
}
556+
557+
const isMobileViewport = window.innerWidth <= 512
558+
const isPortrait = window.matchMedia?.('(orientation: portrait)').matches
559+
?? window.innerHeight > window.innerWidth
560+
561+
isMobilePortrait.value = isMobileViewport && isPortrait
562+
}
563+
546564
function saveSignature() {
547565
if (signatureElementsStore.success.length) {
548566
showSuccess(signatureElementsStore.success)
@@ -728,6 +746,10 @@ function executeSigningAction(action: string) {
728746
}
729747
730748
onMounted(async () => {
749+
updateOrientationHint()
750+
window.addEventListener('resize', updateOrientationHint, { passive: true })
751+
window.addEventListener('orientationchange', updateOrientationHint)
752+
731753
loading.value = true
732754
signatureElementsStore.signRequestUuid = signRequestUuid.value
733755
signatureElementsStore.loadSignatures()
@@ -775,6 +797,8 @@ watch(signRequestUuid, (newUuid, oldUuid) => {
775797
})
776798
777799
onBeforeUnmount(() => {
800+
window.removeEventListener('resize', updateOrientationHint)
801+
window.removeEventListener('orientationchange', updateOrientationHint)
778802
resetSignMethodsState()
779803
if (unwatchPendingAction) {
780804
unwatchPendingAction()
@@ -792,6 +816,23 @@ defineExpose({
792816
</script>
793817

794818
<style lang="scss" scoped>
819+
.document-sign {
820+
display: flex;
821+
flex-direction: column;
822+
height: 100%;
823+
width: 100%;
824+
overscroll-behavior: contain;
825+
-webkit-user-select: none;
826+
user-select: none;
827+
-webkit-touch-callout: none;
828+
}
829+
830+
.sign-elements {
831+
flex: 1;
832+
overflow: hidden;
833+
width: 100%;
834+
}
835+
795836
.progress-indicator {
796837
font-weight: bold;
797838
color: var(--color-primary-element);

src/views/Validation.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,10 @@ function getValidationErrorMessage(response: ValidationErrorResponse | undefined
231231
const errors = response?.data?.ocs?.data?.errors
232232
if (errors?.length) {
233233
const [firstError] = errors
234-
if (typeof firstError === 'string' && firstError.length > 0) {
235-
return firstError
234+
if (typeof firstError === 'string') {
235+
return firstError || fallback
236236
}
237-
if (typeof firstError?.message === 'string' && firstError.message.length > 0) {
237+
if (firstError?.message) {
238238
return firstError.message
239239
}
240240
}

0 commit comments

Comments
 (0)