fix(settings): re-ask the PIN before a relocked backup merge and keep the PIN cooldown across prompts

A backup merge now asks for the PIN again right before it replaces a
playlist's locks when the app relocked during the import, instead of
relying on the answer given at the start. The wrong-PIN count and the
30-second pause move from the dialog into the lock service, so
dismissing and reopening the prompt no longer resets them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-27 12:56:37 +02:00
1 parent 451b373b1d
commit 8ad8907ef9
11 files changed
+237 -19

No files matched your search

+1
View File
@@ -29,6 +29,7 @@ export * from './lib/language.enum';
export * from './lib/m3u-favorite-channel.interface';
export * from './lib/parental-lock.util';
export * from './lib/parental-lock-pin.util';
export * from './lib/parental-lock-pin-throttle.util';
export * from './lib/parsed-playlist.interface';
export * from './lib/performance-phase.interface';
export * from './lib/playback-position.interface';
@@ -0,0 +1,34 @@
import {
createParentalLockPinThrottle,
PARENTAL_LOCK_PIN_COOLDOWN_MS,
PARENTAL_LOCK_PIN_MAX_ATTEMPTS,
} from './parental-lock-pin-throttle.util';
describe('createParentalLockPinThrottle', () => {
it('starts a cooldown after the maximum failures and ends it on time', () => {
let clock = 1_000;
const throttle = createParentalLockPinThrottle(() => clock);
for (let i = 1; i < PARENTAL_LOCK_PIN_MAX_ATTEMPTS; i++) {
expect(throttle.recordFailure()).toBe(false);
}
expect(throttle.cooldownUntil()).toBe(0);
expect(throttle.recordFailure()).toBe(true);
expect(throttle.cooldownUntil()).toBe(
1_000 + PARENTAL_LOCK_PIN_COOLDOWN_MS
);
clock += PARENTAL_LOCK_PIN_COOLDOWN_MS;
expect(throttle.cooldownUntil()).toBe(0);
});
it('clears the count and the cooldown after the right PIN', () => {
const throttle = createParentalLockPinThrottle(() => 0);
for (let i = 1; i < PARENTAL_LOCK_PIN_MAX_ATTEMPTS; i++) {
throttle.recordFailure();
}
throttle.recordSuccess();
expect(throttle.recordFailure()).toBe(false);
expect(throttle.cooldownUntil()).toBe(0);
});
});
@@ -0,0 +1,40 @@
/** Failed PIN attempts before the prompt pauses for the cooldown. */
export const PARENTAL_LOCK_PIN_MAX_ATTEMPTS = 5;
export const PARENTAL_LOCK_PIN_COOLDOWN_MS = 30_000;
/**
* Wrong-PIN bookkeeping that outlives a single PIN dialog. Kept by the lock
* service and handed to every unlock prompt, so dismissing the dialog during
* a cooldown and opening it again neither resets the count nor the pause.
*/
export interface ParentalLockPinThrottle {
/** Epoch milliseconds until which no PIN may be tried; 0 when none. */
cooldownUntil(): number;
/** Records a wrong PIN; true when this failure starts a cooldown. */
recordFailure(): boolean;
/** Clears the count after the right PIN. */
recordSuccess(): void;
}
export function createParentalLockPinThrottle(
now: () => number = Date.now
): ParentalLockPinThrottle {
let failedAttempts = 0;
let cooldownUntil = 0;
return {
cooldownUntil: () => (cooldownUntil > now() ? cooldownUntil : 0),
recordFailure: () => {
failedAttempts += 1;
if (failedAttempts < PARENTAL_LOCK_PIN_MAX_ATTEMPTS) {
return false;
}
failedAttempts = 0;
cooldownUntil = now() + PARENTAL_LOCK_PIN_COOLDOWN_MS;
return true;
},
recordSuccess: () => {
failedAttempts = 0;
cooldownUntil = 0;
},
};
}