From bd75b787849799d6d658214421c094aa240a24f2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:49:52 -0300 Subject: [PATCH 01/10] chore(types): add show-confetti to LibresignCapabilities type Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/ResponseDefinitions.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 519e222f2a..a99123e43f 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -392,6 +392,7 @@ * @psalm-type LibresignCapabilities = array{ * features: list, * config: array{ + * show-confetti: bool, * sign-elements: array{ * is-available: bool, * can-create-signature: bool, From 0ba8ef8fde5e1a450251759ee4c28e06e9b9b912 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:50:14 -0300 Subject: [PATCH 02/10] feat(capabilities): expose show_confetti_after_signing as show-confetti capability Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Capabilities.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 2a6acf056f..b024f13ee7 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -8,11 +8,13 @@ namespace OCA\Libresign; +use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Service\Envelope\EnvelopeService; use OCA\Libresign\Service\SignatureTextService; use OCA\Libresign\Service\SignerElementsService; use OCP\App\IAppManager; use OCP\Capabilities\IPublicCapability; +use OCP\IAppConfig; /** * @psalm-import-type LibresignCapabilities from ResponseDefinitions @@ -27,6 +29,7 @@ public function __construct( protected SignatureTextService $signatureTextService, protected IAppManager $appManager, protected EnvelopeService $envelopeService, + protected IAppConfig $appConfig, ) { } @@ -40,6 +43,7 @@ public function getCapabilities(): array { $capabilities = [ 'features' => self::FEATURES, 'config' => [ + 'show-confetti' => $this->appConfig->getValueBool(Application::APP_ID, 'show_confetti_after_signing', true), 'sign-elements' => [ 'is-available' => $this->signerElementsService->isSignElementsAvailable(), 'can-create-signature' => $this->signerElementsService->canCreateSignature(), From a05456459dad3bd0cea8c3aeea49a688e30a35d1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:50:20 -0300 Subject: [PATCH 03/10] feat(admin): provide show_confetti_after_signing as initial state Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Settings/Admin.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index 6e81475c68..d681e2cb75 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -89,6 +89,7 @@ public function getForm(): TemplateResponse { $this->initialState->provideInitialState('approval_group', $this->appConfig->getValueArray(Application::APP_ID, 'approval_group', ['admin'])); $this->initialState->provideInitialState('envelope_enabled', $this->appConfig->getValueBool(Application::APP_ID, 'envelope_enabled', true)); $this->initialState->provideInitialState('parallel_workers', $this->appConfig->getValueString(Application::APP_ID, 'parallel_workers', '4')); + $this->initialState->provideInitialState('show_confetti_after_signing', $this->appConfig->getValueBool(Application::APP_ID, 'show_confetti_after_signing', true)); return new TemplateResponse(Application::APP_ID, 'admin_settings'); } From 10eda0aeddba6383f56723c86f8755049081b16f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:50:28 -0300 Subject: [PATCH 04/10] feat(settings): add Confetti admin settings component Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/views/Settings/Confetti.vue | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/views/Settings/Confetti.vue diff --git a/src/views/Settings/Confetti.vue b/src/views/Settings/Confetti.vue new file mode 100644 index 0000000000..4f95e9deaf --- /dev/null +++ b/src/views/Settings/Confetti.vue @@ -0,0 +1,41 @@ + + + From 9aac3349b71a5a17fe358344f9deb6a5d6e78ab1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:50:33 -0300 Subject: [PATCH 05/10] feat(settings): register Confetti component in admin settings page Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/views/Settings/Settings.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/views/Settings/Settings.vue b/src/views/Settings/Settings.vue index 1c16a3fc64..a7c0a21ef7 100644 --- a/src/views/Settings/Settings.vue +++ b/src/views/Settings/Settings.vue @@ -28,6 +28,7 @@ + @@ -53,6 +54,7 @@ import SignatureHashAlgorithm from './SignatureHashAlgorithm.vue' import CollectMetadata from './CollectMetadata.vue' import SignatureStamp from './SignatureStamp.vue' import SigningMode from './SigningMode.vue' +import Confetti from './Confetti.vue' import Envelope from './Envelope.vue' import SupportProject from './SupportProject.vue' import TSA from './TSA.vue' @@ -79,6 +81,7 @@ export default { SignatureHashAlgorithm, CollectMetadata, SignatureStamp, + Confetti, SigningMode, Envelope, SupportProject, From 3a2a9dcb0d7ecc0e8b6c59bcff2c44c3dcf7c066 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:50:41 -0300 Subject: [PATCH 06/10] feat(validation): gate confetti animation behind show-confetti capability Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/views/Validation.vue | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/views/Validation.vue b/src/views/Validation.vue index 6913088999..3049f25582 100644 --- a/src/views/Validation.vue +++ b/src/views/Validation.vue @@ -86,6 +86,7 @@ import { } from '@mdi/js' import JSConfetti from 'js-confetti' import axios from '@nextcloud/axios' +import { getCapabilities } from '@nextcloud/capabilities' import { formatFileSize } from '@nextcloud/files' import { loadState } from '@nextcloud/initial-state' import { t } from '@nextcloud/l10n' @@ -614,8 +615,10 @@ export default { && this.document.files.every(file => isSignedStatus(file.status)) const signerCompleted = this.isCurrentSignerSigned() if ((isSignedDoc || allFilesSigned || signerCompleted) && (this.isAfterSigned || this.shouldFireAsyncConfetti)) { - const jsConfetti = new JSConfetti() - jsConfetti.addConfetti() + if (getCapabilities()?.libresign?.config?.['show-confetti'] === true) { + const jsConfetti = new JSConfetti() + jsConfetti.addConfetti() + } this.shouldFireAsyncConfetti = false } }, From 42676b510ab81cff5444dbeb4cbdc9eac0b1d8e2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:50:47 -0300 Subject: [PATCH 07/10] test(capabilities): update and add tests for show-confetti capability Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/CapabilitiesTest.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/php/Unit/CapabilitiesTest.php b/tests/php/Unit/CapabilitiesTest.php index b2286919c1..9a3f5fd7d8 100644 --- a/tests/php/Unit/CapabilitiesTest.php +++ b/tests/php/Unit/CapabilitiesTest.php @@ -9,11 +9,13 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Capabilities; use OCA\Libresign\Service\Envelope\EnvelopeService; use OCA\Libresign\Service\SignatureTextService; use OCA\Libresign\Service\SignerElementsService; use OCP\App\IAppManager; +use OCP\IAppConfig; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; @@ -23,12 +25,14 @@ final class CapabilitiesTest extends \OCA\Libresign\Tests\Unit\TestCase { private SignatureTextService&MockObject $signatureTextService; private IAppManager&MockObject $appManager; private EnvelopeService&MockObject $envelopeService; + private IAppConfig&MockObject $appConfig; public function setUp(): void { $this->signerElementsService = $this->createMock(SignerElementsService::class); $this->signatureTextService = $this->createMock(SignatureTextService::class); $this->appManager = $this->createMock(IAppManager::class); $this->envelopeService = $this->createMock(EnvelopeService::class); + $this->appConfig = $this->createMock(IAppConfig::class); } @@ -38,6 +42,7 @@ private function getClass(): Capabilities { $this->signatureTextService, $this->appManager, $this->envelopeService, + $this->appConfig, ); return $this->capabilities; } @@ -55,4 +60,20 @@ public static function providerSignElementsIsAvailable(): array { [false, false], ]; } + + #[DataProvider('providerShowConfetti')] + public function testShowConfetti(bool $configValue, bool $expected): void { + $this->appConfig->method('getValueBool') + ->with(Application::APP_ID, 'show_confetti_after_signing', true) + ->willReturn($configValue); + $capabilities = $this->getClass()->getCapabilities(); + $this->assertEquals($expected, $capabilities['libresign']['config']['show-confetti']); + } + + public static function providerShowConfetti(): array { + return [ + [true, true], + [false, false], + ]; + } } From 4d107590bc743fbf3588b4ea5f3c772bc4ab1cc1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:50:53 -0300 Subject: [PATCH 08/10] test(settings): add tests for Confetti admin settings component Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/tests/views/Settings/Confetti.spec.ts | 111 ++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/tests/views/Settings/Confetti.spec.ts diff --git a/src/tests/views/Settings/Confetti.spec.ts b/src/tests/views/Settings/Confetti.spec.ts new file mode 100644 index 0000000000..5aed6194b1 --- /dev/null +++ b/src/tests/views/Settings/Confetti.spec.ts @@ -0,0 +1,111 @@ +/* + * SPDX-FileCopyrightText: 2026 LibreSign contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { mount } from '@vue/test-utils' + +const loadStateMock = vi.fn() + +vi.mock('@nextcloud/initial-state', () => ({ + loadState: (...args: unknown[]) => loadStateMock(...args), +})) + +vi.mock('@nextcloud/l10n', () => ({ + t: vi.fn((_app: string, text: string) => text), + translate: vi.fn((_app: string, text: string) => text), + translatePlural: vi.fn((_app: string, singular: string, plural: string, count: number) => (count === 1 ? singular : plural)), + n: vi.fn((_app: string, singular: string, plural: string, count: number) => (count === 1 ? singular : plural)), + isRTL: vi.fn(() => false), + getLanguage: vi.fn(() => 'en'), + getLocale: vi.fn(() => 'en'), +})) + +let Confetti: unknown + +beforeAll(async () => { + ;({ default: Confetti } = await import('../../../views/Settings/Confetti.vue')) +}) + +describe('Confetti', () => { + beforeEach(() => { + loadStateMock.mockReset() + }) + + it('defaults to true when state is not set', async () => { + loadStateMock.mockImplementation((_app: string, key: string, fallback: unknown) => fallback) + + const wrapper = mount(Confetti as never, { + global: { + stubs: { + NcSettingsSection: { template: '
' }, + NcCheckboxRadioSwitch: { template: '
' }, + }, + }, + }) + + expect(wrapper.vm.showConfetti).toBe(true) + }) + + it('reads show_confetti_after_signing from initial state', async () => { + loadStateMock.mockImplementation((_app: string, key: string, fallback: unknown) => { + if (key === 'show_confetti_after_signing') return true + return fallback + }) + + const wrapper = mount(Confetti as never, { + global: { + stubs: { + NcSettingsSection: { template: '
' }, + NcCheckboxRadioSwitch: { template: '
' }, + }, + }, + }) + + expect(wrapper.vm.showConfetti).toBe(true) + }) + + it('calls OCP.AppConfig.setValue with "1" when enabled', async () => { + loadStateMock.mockImplementation((_app: string, key: string, fallback: unknown) => fallback) + const setValueMock = vi.fn() + vi.stubGlobal('OCP', { AppConfig: { setValue: setValueMock } }) + + const wrapper = mount(Confetti as never, { + global: { + stubs: { + NcSettingsSection: { template: '
' }, + NcCheckboxRadioSwitch: { template: '
' }, + }, + }, + }) + + await wrapper.setData({ showConfetti: true }) + wrapper.vm.saveShowConfetti() + + expect(setValueMock).toHaveBeenCalledWith('libresign', 'show_confetti_after_signing', '1') + }) + + it('calls OCP.AppConfig.setValue with "0" when disabled', async () => { + loadStateMock.mockImplementation((_app: string, key: string, fallback: unknown) => { + if (key === 'show_confetti_after_signing') return true + return fallback + }) + const setValueMock = vi.fn() + vi.stubGlobal('OCP', { AppConfig: { setValue: setValueMock } }) + + const wrapper = mount(Confetti as never, { + global: { + stubs: { + NcSettingsSection: { template: '
' }, + NcCheckboxRadioSwitch: { template: '
' }, + }, + }, + }) + + await wrapper.setData({ showConfetti: false }) + wrapper.vm.saveShowConfetti() + + expect(setValueMock).toHaveBeenCalledWith('libresign', 'show_confetti_after_signing', '0') + }) +}) From 745d2e695f92f1c1ca8f75688c2a35cb76fc76b2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:50:58 -0300 Subject: [PATCH 09/10] test(validation): mock show-confetti capability and add disabled scenario test Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/tests/views/Validation.spec.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/tests/views/Validation.spec.ts b/src/tests/views/Validation.spec.ts index 089f19ce5b..094ae604cb 100644 --- a/src/tests/views/Validation.spec.ts +++ b/src/tests/views/Validation.spec.ts @@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, beforeEach, vi } from 'vitest' import { shallowMount } from '@vue/test-utils' import axios from '@nextcloud/axios' +import { getCapabilities } from '@nextcloud/capabilities' import JSConfetti from 'js-confetti' import Validation from '../../views/Validation.vue' @@ -104,6 +105,17 @@ const mockRouter = { push: vi.fn(), } +// Mock capabilities - show-confetti enabled by default so existing tests pass +vi.mock('@nextcloud/capabilities', () => ({ + getCapabilities: vi.fn(() => ({ + libresign: { + config: { + 'show-confetti': true, + }, + }, + })), +})) + // Mock initial state vi.mock('@nextcloud/initial-state', () => ({ loadState: vi.fn((app, key, defaultValue) => defaultValue), @@ -654,6 +666,19 @@ describe('Validation.vue - Business Logic', () => { wrapper.vm.handleValidationSuccess({ status: SIGNED_STATUS, signers: [] }) expect(mockAddConfetti).not.toHaveBeenCalled() }) + + it('does not fire confetti when show-confetti capability is disabled', async () => { + vi.mocked(getCapabilities).mockReturnValueOnce({ + libresign: { + config: { + 'show-confetti': false, + }, + }, + } as ReturnType) + vi.spyOn(wrapper.vm, 'isAfterSigned', 'get').mockReturnValue(true) + wrapper.vm.handleValidationSuccess({ status: SIGNED_STATUS, signers: [] }) + expect(mockAddConfetti).not.toHaveBeenCalled() + }) }) describe('handleSigningComplete method', () => { From f76791ee703cdbed798544f9f9e9320c9debe4fe Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:52:32 -0300 Subject: [PATCH 10/10] chore: update openapi documentation Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- openapi-administration.json | 4 ++++ openapi-full.json | 4 ++++ openapi.json | 4 ++++ src/types/openapi/openapi-administration.ts | 1 + src/types/openapi/openapi-full.ts | 1 + src/types/openapi/openapi.ts | 1 + 6 files changed, 15 insertions(+) diff --git a/openapi-administration.json b/openapi-administration.json index 81557cbc79..8e75f42a87 100644 --- a/openapi-administration.json +++ b/openapi-administration.json @@ -37,11 +37,15 @@ "config": { "type": "object", "required": [ + "show-confetti", "sign-elements", "envelope", "upload" ], "properties": { + "show-confetti": { + "type": "boolean" + }, "sign-elements": { "type": "object", "required": [ diff --git a/openapi-full.json b/openapi-full.json index a21fe1a024..200bae017c 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -37,11 +37,15 @@ "config": { "type": "object", "required": [ + "show-confetti", "sign-elements", "envelope", "upload" ], "properties": { + "show-confetti": { + "type": "boolean" + }, "sign-elements": { "type": "object", "required": [ diff --git a/openapi.json b/openapi.json index 2fa2516699..3030a0e4c6 100644 --- a/openapi.json +++ b/openapi.json @@ -37,11 +37,15 @@ "config": { "type": "object", "required": [ + "show-confetti", "sign-elements", "envelope", "upload" ], "properties": { + "show-confetti": { + "type": "boolean" + }, "sign-elements": { "type": "object", "required": [ diff --git a/src/types/openapi/openapi-administration.ts b/src/types/openapi/openapi-administration.ts index e4cc84fa82..f8b711aaa0 100644 --- a/src/types/openapi/openapi-administration.ts +++ b/src/types/openapi/openapi-administration.ts @@ -492,6 +492,7 @@ export type components = { Capabilities: { features: string[]; config: { + "show-confetti": boolean; "sign-elements": { "is-available": boolean; "can-create-signature": boolean; diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 61948ed532..c0b4467086 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -1527,6 +1527,7 @@ export type components = { Capabilities: { features: string[]; config: { + "show-confetti": boolean; "sign-elements": { "is-available": boolean; "can-create-signature": boolean; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 222ead5fa6..1232192bf4 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -1046,6 +1046,7 @@ export type components = { Capabilities: { features: string[]; config: { + "show-confetti": boolean; "sign-elements": { "is-available": boolean; "can-create-signature": boolean;