@@ -11,13 +11,12 @@ import 'package:clerk_flutter/src/widgets/ui/closeable.dart';
1111import 'package:clerk_flutter/src/widgets/ui/common.dart' ;
1212import 'package:clerk_flutter/src/widgets/ui/style/colors.dart' ;
1313import 'package:clerk_flutter/src/widgets/ui/style/text_style.dart' ;
14+ import 'package:collection/collection.dart' ;
1415import 'package:flutter/gestures.dart' ;
1516import 'package:flutter/material.dart' ;
1617import 'package:phone_input/phone_input_package.dart' ;
1718import 'package:url_launcher/url_launcher_string.dart' ;
1819
19- typedef _ValueChanger = void Function (String value);
20-
2120enum _SignUpPanelState {
2221 input,
2322 waiting;
@@ -50,7 +49,6 @@ class _ClerkSignUpPanelState extends State<ClerkSignUpPanel>
5049
5150 _SignUpPanelState _state = _SignUpPanelState .input;
5251 final Map <clerk.UserAttribute , String ?> _values = {};
53- bool _isObscured = true ;
5452 bool _needsLegalAcceptance = true ;
5553 bool _hasLegalAcceptance = false ;
5654 bool _highlightMissing = false ;
@@ -166,17 +164,11 @@ class _ClerkSignUpPanelState extends State<ClerkSignUpPanel>
166164 });
167165 }
168166
169- void _onObscure () => setState (() => _isObscured = ! _isObscured);
170-
171167 void _acceptTerms () =>
172168 setState (() => _hasLegalAcceptance = ! _hasLegalAcceptance);
173169
174170 void _reset () => setState (() => _state = _SignUpPanelState .input);
175171
176- _ValueChanger _change (clerk.UserAttribute attr) => (String value) {
177- _values[attr] = value;
178- };
179-
180172 @override
181173 Widget build (BuildContext context) {
182174 final authState = ClerkAuth .of (context);
@@ -193,19 +185,19 @@ class _ClerkSignUpPanelState extends State<ClerkSignUpPanel>
193185 case clerk.UserAttributeData data when data.isEnabled) //
194186 _Attribute (attr, data),
195187 ];
196- final lastNameAttr = attributes.any ((a) => a.isFirstName)
197- ? attributes.removeFirstOrNull ((a) => a.isLastName)
198- : null ;
199188 final isAwaitingCode = (env.supportsEmailCode &&
200189 signUp? .unverified (clerk.Field .emailAddress) == true ) ||
201190 (env.supportsPhoneCode &&
202191 signUp? .unverified (clerk.Field .phoneNumber) == true );
203192
204- bool isMissing (_Attribute attribute) =>
205- signUp? .missing (clerk.Field .forUserAttribute (attribute.attr)) == true ||
206- (_highlightMissing &&
207- attribute.isRequired &&
208- _valueOrNull (attribute.attr) == null );
193+ // if we have both first and last name, associate them
194+ attributes.firstWhereOrNull ((a) => a.isFirstName)? .associated =
195+ attributes.removeFirstOrNull ((a) => a.isLastName);
196+
197+ // if we have a password, associate a confirmation
198+ final password = attributes.firstWhereOrNull ((a) => a.isPassword);
199+ password? .associated =
200+ _Attribute (clerk.UserAttribute .passwordConfirmation, password.data);
209201
210202 return Column (
211203 crossAxisAlignment: CrossAxisAlignment .stretch,
@@ -257,74 +249,16 @@ class _ClerkSignUpPanelState extends State<ClerkSignUpPanel>
257249 Closeable (
258250 closed: _state.isWaiting,
259251 child: Column (
252+ mainAxisSize: MainAxisSize .min,
260253 children: [
261- for (final attribute in attributes) ...[
262- if (attribute.isPhoneNumber) //
263- ClerkPhoneNumberFormField (
264- initial: _values[clerk.UserAttribute .phoneNumber],
265- label: attribute.title (l10ns),
266- isMissing: isMissing (attribute),
267- isOptional: attribute.isOptional,
268- onChanged: _change (clerk.UserAttribute .phoneNumber),
269- )
270- else if (attribute.isFirstName) //
271- Row (
272- mainAxisSize: MainAxisSize .min,
273- children: [
274- Expanded (
275- child: ClerkTextFormField (
276- initial: _values[attribute.attr],
277- label: attribute.title (l10ns),
278- isMissing: isMissing (attribute),
279- isOptional: attribute.isOptional,
280- onChanged: _change (attribute.attr),
281- ),
282- ),
283- if (lastNameAttr case final lastNameAttr? ) ...[
284- horizontalMargin16,
285- Expanded (
286- child: ClerkTextFormField (
287- initial: _values[lastNameAttr.attr],
288- label: lastNameAttr.title (l10ns),
289- isMissing: isMissing (lastNameAttr),
290- isOptional: lastNameAttr.isOptional,
291- onChanged: _change (lastNameAttr.attr),
292- ),
293- ),
294- ],
295- ],
296- )
297- else if (attribute.isPassword) ...[
298- ClerkTextFormField (
299- initial: _values[clerk.UserAttribute .password],
300- label: attribute.title (l10ns),
301- isMissing: isMissing (attribute),
302- isOptional: attribute.isOptional,
303- obscureText: _isObscured,
304- onObscure: _onObscure,
305- onChanged: _change (clerk.UserAttribute .password),
306- ),
307- verticalMargin16,
308- ClerkTextFormField (
309- initial: _values[clerk.UserAttribute .passwordConfirmation],
310- label: l10ns.grammar.toSentence (l10ns.passwordConfirmation),
311- isMissing: isMissing (attribute),
312- isOptional: attribute.isOptional,
313- obscureText: _isObscured,
314- onObscure: _onObscure,
315- onChanged:
316- _change (clerk.UserAttribute .passwordConfirmation),
317- ),
318- ] else
319- ClerkTextFormField (
320- initial: _values[attribute.attr],
321- label: attribute.title (l10ns),
322- isMissing: isMissing (attribute),
323- isOptional: attribute.isOptional,
324- onChanged: _change (attribute.attr),
325- ),
326- verticalMargin16,
327- ],
254+ for (final attribute in attributes) //
255+ _FormField (
256+ attribute: attribute,
257+ authState: authState,
258+ localizations: l10ns,
259+ values: _values,
260+ highlight: _highlightMissing,
261+ ),
328262 ],
329263 ),
330264 ),
@@ -374,13 +308,97 @@ class _ClerkSignUpPanelState extends State<ClerkSignUpPanel>
374308 }
375309}
376310
311+ class _FormField extends StatelessWidget {
312+ const _FormField ({
313+ required this .attribute,
314+ required this .authState,
315+ required this .localizations,
316+ required this .values,
317+ required this .highlight,
318+ });
319+
320+ final _Attribute attribute;
321+
322+ final ClerkAuthState authState;
323+
324+ final ClerkSdkLocalizations localizations;
325+
326+ final Map <clerk.UserAttribute , String ?> values;
327+
328+ final bool highlight;
329+
330+ static final _obscure = ValueNotifier (true );
331+
332+ bool _isMissing (ClerkAuthState authState, _Attribute attribute) =>
333+ authState.signUp? .missing (clerk.Field .forUserAttribute (attribute.attr)) ==
334+ true ||
335+ (highlight &&
336+ attribute.isRequired &&
337+ (values[attribute.attr]? .trim () ?? '' ).isEmpty);
338+
339+ Widget _formField (_Attribute attribute) {
340+ if (attribute.needsObscuring) {
341+ return ValueListenableBuilder (
342+ valueListenable: _obscure,
343+ builder: (context, obscure, _) {
344+ return ClerkTextFormField (
345+ initial: values[attribute.attr],
346+ label: attribute.title (localizations),
347+ obscureText: obscure,
348+ onObscure: () => _obscure.value = ! obscure,
349+ isMissing: _isMissing (authState, attribute),
350+ onChanged: (value) => values[attribute.attr] = value,
351+ );
352+ },
353+ );
354+ }
355+
356+ return ClerkTextFormField (
357+ initial: values[attribute.attr],
358+ label: attribute.title (localizations),
359+ isMissing: _isMissing (authState, attribute),
360+ onChanged: (value) => values[attribute.attr] = value,
361+ );
362+ }
363+
364+ @override
365+ Widget build (BuildContext context) {
366+ return Padding (
367+ padding: bottomPadding16,
368+ child: switch (attribute) {
369+ _Attribute attribute when attribute.isPhoneNumber =>
370+ ClerkPhoneNumberFormField (
371+ initial: values[attribute.attr],
372+ label: attribute.title (localizations),
373+ isMissing: _isMissing (authState, attribute),
374+ isOptional: attribute.isOptional,
375+ onChanged: (value) => values[attribute.attr] = value,
376+ ),
377+ _Attribute attribute when attribute.associated is _Attribute => Flex (
378+ direction: attribute.isFirstName ? Axis .horizontal : Axis .vertical,
379+ mainAxisSize: MainAxisSize .min,
380+ children: [
381+ Flexible (fit: FlexFit .loose, child: _formField (attribute)),
382+ const SizedBox .square (dimension: 16 ),
383+ Flexible (
384+ fit: FlexFit .loose,
385+ child: _formField (attribute.associated! ),
386+ ),
387+ ],
388+ ),
389+ _Attribute attribute => _formField (attribute),
390+ },
391+ );
392+ }
393+ }
394+
377395class _LegalAcceptanceConfirmation extends StatelessWidget {
378396 const _LegalAcceptanceConfirmation ();
379397
380398 List <TextSpan > _subSpans (String text, String target, String ? url) {
381- if (url case String url) {
399+ if (url case String url when url.isNotEmpty ) {
382400 final segments = text.split (target);
383- final spans = < TextSpan > [TextSpan (text: segments.first)];
401+ final spans = [TextSpan (text: segments.first)];
384402
385403 for (final segmentText in segments.skip (1 )) {
386404 spans.add (
@@ -507,12 +525,14 @@ class _CodeInputBoxState extends State<_CodeInputBox> {
507525}
508526
509527class _Attribute {
510- const _Attribute (this .attr, this .data);
528+ _Attribute (this .attr, this .data);
511529
512530 final clerk.UserAttribute attr;
513531
514532 final clerk.UserAttributeData data;
515533
534+ _Attribute ? associated;
535+
516536 bool get isPhoneNumber => attr == clerk.UserAttribute .phoneNumber;
517537
518538 bool get isPassword => attr == clerk.UserAttribute .password;
@@ -525,6 +545,9 @@ class _Attribute {
525545
526546 bool get isOptional => isRequired == false ;
527547
548+ bool get needsObscuring =>
549+ isPassword || attr == clerk.UserAttribute .passwordConfirmation;
550+
528551 String title (ClerkSdkLocalizations l10ns) =>
529552 l10ns.grammar.toSentence (attr.localizedMessage (l10ns));
530553}
0 commit comments