From 5392692e2a9e366d490c2f87f154945f0224c4e7 Mon Sep 17 00:00:00 2001 From: Wes Johnson Date: Tue, 9 Sep 2025 19:01:28 -0400 Subject: [PATCH 1/3] fix(ios): avoid min>max date picker crash --- ios/fabric/RNDateTimePickerComponentView.mm | 27 ++++++--- src/datetimepicker.ios.js | 2 +- src/utils.js | 12 ++++ test/utils.test.js | 62 ++++++++++++++++++++- 4 files changed, 94 insertions(+), 9 deletions(-) diff --git a/ios/fabric/RNDateTimePickerComponentView.mm b/ios/fabric/RNDateTimePickerComponentView.mm index 2142b32a..358ec22d 100644 --- a/ios/fabric/RNDateTimePickerComponentView.mm +++ b/ios/fabric/RNDateTimePickerComponentView.mm @@ -174,13 +174,26 @@ - (Boolean)updatePropsForPicker:(UIDatePicker *)picker props:(Props::Shared cons needsToUpdateMeasurements = true; } - if (oldPickerProps.minimumDate != newPickerProps.minimumDate) { - NSDate *minimumDate = convertJSTimeToDate(newPickerProps.minimumDate); - picker.minimumDate = adjustMinimumDate(minimumDate, newPickerProps.minuteInterval); - } - - if (oldPickerProps.maximumDate != newPickerProps.maximumDate) { - picker.maximumDate = convertJSTimeToDate(newPickerProps.maximumDate); + if (oldPickerProps.minimumDate != newPickerProps.minimumDate || + oldPickerProps.maximumDate != newPickerProps.maximumDate) { + + NSDate *newMinDate = convertJSTimeToDate(newPickerProps.minimumDate); + NSDate *newMaxDate = convertJSTimeToDate(newPickerProps.maximumDate); + + if (newMinDate) { + newMinDate = adjustMinimumDate(newMinDate, newPickerProps.minuteInterval); + } + + // avoid crash when min > max by ensuring a clean initial state + picker.minimumDate = nil; + picker.maximumDate = nil; + + // set the dates in all cases (whether unset/nil, some set, or both set) + // UNLESS min > max, then we leave them as nil and rely on our LogBox in JS + if (!newMinDate || !newMaxDate || [newMinDate compare:newMaxDate] != NSOrderedDescending) { + picker.minimumDate = newMinDate; + picker.maximumDate = newMaxDate; + } } if (oldPickerProps.locale != newPickerProps.locale) { diff --git a/src/datetimepicker.ios.js b/src/datetimepicker.ios.js index f9644a18..d8819f76 100644 --- a/src/datetimepicker.ios.js +++ b/src/datetimepicker.ios.js @@ -61,7 +61,7 @@ export default function Picker({ disabled = false, ...other }: IOSNativeProps): React.Node { - sharedPropsValidation({value, timeZoneOffsetInMinutes, timeZoneName}); + sharedPropsValidation({value, timeZoneOffsetInMinutes, timeZoneName, minimumDate, maximumDate}); const display = getDisplaySafe(providedDisplay); diff --git a/src/utils.js b/src/utils.js index f096cc41..e8f7cac9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -35,10 +35,14 @@ export function sharedPropsValidation({ value, timeZoneName, timeZoneOffsetInMinutes, + minimumDate, + maximumDate, }: { value: Date, timeZoneName?: ?string, timeZoneOffsetInMinutes?: ?number, + minimumDate?: ?Date, + maximumDate?: ?Date, }) { invariant(value, 'A date or time must be specified as `value` prop'); invariant( @@ -49,6 +53,14 @@ export function sharedPropsValidation({ timeZoneName == null || timeZoneOffsetInMinutes == null, '`timeZoneName` and `timeZoneOffsetInMinutes` cannot be specified at the same time', ); + + if (minimumDate && maximumDate) { + invariant( + minimumDate <= maximumDate, + `DateTimePicker: minimumDate (${minimumDate.toISOString()}) is after maximumDate (${maximumDate.toISOString()}). Please ensure minimumDate <= maximumDate.`, + ); + } + if (timeZoneOffsetInMinutes !== undefined) { console.warn( '`timeZoneOffsetInMinutes` is deprecated and will be removed in a future release. Use `timeZoneName` instead.', diff --git a/test/utils.test.js b/test/utils.test.js index 0a10e9c2..b3b1a1a6 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -1,4 +1,4 @@ -import {toMilliseconds} from '../src/utils.js'; +import {toMilliseconds, sharedPropsValidation} from '../src/utils.js'; describe('utils', () => { describe('toMilliseconds', () => { @@ -17,4 +17,64 @@ describe('utils', () => { expect(options).toHaveProperty('maximumDate', 2556057600000); }); }); + + describe('sharedPropsValidation', () => { + describe('minimumDate and maximumDate validation', () => { + it('should not throw when dates are in correct order', () => { + const value = new Date('2023-06-15'); + const minimumDate = new Date('2023-01-01'); + const maximumDate = new Date('2023-12-31'); + + expect(() => { + sharedPropsValidation({value, minimumDate, maximumDate}); + }).not.toThrow(); + }); + + it('should not throw when dates are equal', () => { + const value = new Date('2023-06-15'); + const minimumDate = new Date('2023-06-15'); + const maximumDate = new Date('2023-06-15'); + + expect(() => { + sharedPropsValidation({value, minimumDate, maximumDate}); + }).not.toThrow(); + }); + + it('should not throw when only minimumDate is provided', () => { + const value = new Date('2023-06-15'); + const minimumDate = new Date('2023-01-01'); + + expect(() => { + sharedPropsValidation({value, minimumDate}); + }).not.toThrow(); + }); + + it('should not throw when only maximumDate is provided', () => { + const value = new Date('2023-06-15'); + const maximumDate = new Date('2023-12-31'); + + expect(() => { + sharedPropsValidation({value, maximumDate}); + }).not.toThrow(); + }); + + it('should not throw when neither minimumDate nor maximumDate is provided', () => { + const value = new Date('2023-06-15'); + + expect(() => { + sharedPropsValidation({value}); + }).not.toThrow(); + }); + + it('should throw when minimumDate is after maximumDate', () => { + const value = new Date('2023-06-15'); + const minimumDate = new Date('2023-12-31'); + const maximumDate = new Date('2023-01-01'); + + expect(() => { + sharedPropsValidation({value, minimumDate, maximumDate}); + }).toThrow('DateTimePicker: minimumDate (2023-12-31T00:00:00.000Z) is after maximumDate (2023-01-01T00:00:00.000Z). Please ensure minimumDate <= maximumDate.'); + }); + }); + }); }); From cd57fa0a59af429167cc5d8454cfa944646877c0 Mon Sep 17 00:00:00 2001 From: Wes Johnson Date: Tue, 9 Sep 2025 20:44:52 -0400 Subject: [PATCH 2/3] fix zero value handling --- ios/fabric/RNDateTimePickerComponentView.mm | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ios/fabric/RNDateTimePickerComponentView.mm b/ios/fabric/RNDateTimePickerComponentView.mm index 358ec22d..3afc9c09 100644 --- a/ios/fabric/RNDateTimePickerComponentView.mm +++ b/ios/fabric/RNDateTimePickerComponentView.mm @@ -174,11 +174,12 @@ - (Boolean)updatePropsForPicker:(UIDatePicker *)picker props:(Props::Shared cons needsToUpdateMeasurements = true; } - if (oldPickerProps.minimumDate != newPickerProps.minimumDate || - oldPickerProps.maximumDate != newPickerProps.maximumDate) { - - NSDate *newMinDate = convertJSTimeToDate(newPickerProps.minimumDate); - NSDate *newMaxDate = convertJSTimeToDate(newPickerProps.maximumDate); + Boolean minDateChanged = oldPickerProps.minimumDate != newPickerProps.minimumDate; + Boolean maxDateChanged = oldPickerProps.maximumDate != newPickerProps.maximumDate; + + if (minDateChanged || maxDateChanged) { + NSDate *newMinDate = newPickerProps.minimumDate ? convertJSTimeToDate(newPickerProps.minimumDate) : nil; + NSDate *newMaxDate = newPickerProps.maximumDate ? convertJSTimeToDate(newPickerProps.maximumDate) : nil; if (newMinDate) { newMinDate = adjustMinimumDate(newMinDate, newPickerProps.minuteInterval); From 393bab892d66faf9511f96688d4e3920d98893ff Mon Sep 17 00:00:00 2001 From: Wes Johnson Date: Wed, 10 Sep 2025 07:50:00 -0400 Subject: [PATCH 3/3] update example, spec --- example/App.js | 18 ++++++++++++++++++ src/utils.js | 2 +- test/utils.test.js | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/example/App.js b/example/App.js index c52ee323..e1c96103 100644 --- a/example/App.js +++ b/example/App.js @@ -471,6 +471,24 @@ export const App = () => { title="toggleMinMaxDate" /> + {/* This button allows for testing the error box that should show when the minimumDate prop is greater than the maximumDate prop */} + +