Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/handbook-build-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ jobs:
set -euo pipefail
bash scripts/assemble-handbook-screenshots.sh /tmp/handbook-shots
count=$(ls -1 /tmp/handbook-shots/*.png | wc -l | tr -d ' ')
if [ "$count" != "279" ]; then
echo "expected 279 screenshots, got $count" >&2
if [ "$count" != "278" ]; then
echo "expected 278 screenshots, got $count" >&2
exit 1
fi

Expand Down Expand Up @@ -173,7 +173,7 @@ jobs:
exit 1
fi

# Screenshots dir must contain all 279 PNGs assembled from Goldens.
# Screenshots dir must contain all 278 PNGs assembled from Goldens.
# Hit one of them through the auth gate to verify wiring end-to-end.
# Mix of the original 01-61 range and the 62-269 batch (every Golden
# baseline) so a regression in either half surfaces here.
Expand Down
2 changes: 2 additions & 0 deletions assets/languages/strings_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@
"registerEmailVerificationTitle": "Willkommen zurück!",
"registerPhoneNumberInvalid": "Telefonnummer ist erforderlich",
"registerPhoneNumberOnlyDigits": "Nur Zahlen sind erlaubt",
"registerPhoneNumberPrefixFormat": "Vorwahl muss aus 1 bis 3 Ziffern bestehen",
"registerPhoneNumberPrefixInvalid": "Vorwahl ist erforderlich",
"registrationFailed": "Registrierung fehlgeschlagen:\n${message}",
"registrationForwardingFailed": "Registrierung angenommen, aber die Weiterleitung an die Gesellschaft ist verzögert. Wir versuchen es automatisch erneut.",
"registrationRejected": "Der Server hat Ihre Registrierung abgelehnt:\n${reason}\nIhre Daten wurden nicht gespeichert — bitte prüfen Sie Ihre Angaben und senden Sie erneut ab.",
Expand Down
2 changes: 2 additions & 0 deletions assets/languages/strings_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@
"registerEmailVerificationTitle": "Welcome back!",
"registerPhoneNumberInvalid": "Phone number is required",
"registerPhoneNumberOnlyDigits": "Only numbers are allowed",
"registerPhoneNumberPrefixFormat": "Country code must be 1 to 3 digits",
"registerPhoneNumberPrefixInvalid": "Country code is required",
"registrationFailed": "Registration failed:\n${message}",
"registrationForwardingFailed": "Registration accepted, but forwarding to the company is delayed. We will retry automatically.",
"registrationRejected": "The server rejected your registration:\n${reason}\nYour data has not been saved — please check your entries and submit again.",
Expand Down
2 changes: 1 addition & 1 deletion docs/handbook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ deployten Image (`handbook.realunit.app`).

## Screenshots regenerieren

Es gibt keinen separaten Regeneration-Schritt: Die 279 Handbook-Screenshots
Es gibt keinen separaten Regeneration-Schritt: Die 278 Handbook-Screenshots
sind direkt die Golden-Baselines unter `test/goldens/` (gemappt in
`scripts/assemble-handbook-screenshots.sh`). Eine UI-Änderung an einer der
gemappten Pages produziert beim `flutter test test/goldens` einen Diff —
Expand Down
16 changes: 0 additions & 16 deletions docs/handbook/de/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5588,22 +5588,6 @@ <h2><span class="num">53</span>KYC — Registrierung</h2>
Das geöffnete <b>Kontotyp-Auswahlmenü</b> im persönlichen Schritt. Das Overlay zeigt die auswählbaren Kontotypen.
</div>
</div>
<div class="test" id="164-kyc-registration-personal-step-phone-prefix-open">
<div class="head">
<a class="name permalink" href="#164-kyc-registration-personal-step-phone-prefix-open">164-kyc-registration-personal-step-phone-prefix-open</a>
<button class="copy-link" type="button" data-target="164-kyc-registration-personal-step-phone-prefix-open" title="Direkt-Link kopieren" aria-label="Direkt-Link kopieren">🔗 Link</button>
<span class="src">screens/kyc/goldens/macos/kyc_registration_personal_step_phone_prefix_open.png</span>
</div>
<div class="img">
<img
src="../screenshots/164-kyc-registration-personal-step-phone-prefix-open.png"
alt="Registrierung — Vorwahl-Dropdown offen"
/>
</div>
<div class="desc">
Das geöffnete Auswahlmenü für die <b>Telefon-Ländervorwahl</b> (z. B. +41 / +49). Der Nutzer wählt hier die Vorwahl vor der Eingabe der Telefonnummer.
</div>
</div>
<div class="test" id="165-kyc-registration-personal-step-validation-error">
<div class="head">
<a class="name permalink" href="#165-kyc-registration-personal-step-validation-error">165-kyc-registration-personal-step-validation-error</a>
Expand Down
4 changes: 2 additions & 2 deletions docs/screens.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Column meaning:
screen, or `—` if the screen has no Golden baseline. Each slot is a
Visual-Regression Golden under `test/goldens/`, mapped to its handbook
position by `scripts/assemble-handbook-screenshots.sh`. The handbook now
covers **all 279 Golden baselines** — every screen **plus every tested
covers **all 278 Golden baselines** — every screen **plus every tested
state variant** (Default / Loading / Error / Snackbar / Dropdown /
Validation / Confirm / Success / Failure …), including the areas that were
previously absent: Support (email capture, tickets, chat), Settings
Expand Down Expand Up @@ -136,7 +136,7 @@ one shared form-widget baseline (`PhoneNumberField`), which is not a screen.
`243`–`245`. `SetupPinPage` also backs the `settingsChangePin` route
(`/settings/security/changePin`) via a second constructor; that reuse has no
separate Golden and is not given its own row.
- **Handbook numbering.** Each of the 279 handbook slots is a Visual-Regression
- **Handbook numbering.** Each of the 278 handbook slots is a Visual-Regression
Golden under `test/goldens/`, mapped to its handbook position by
`scripts/assemble-handbook-screenshots.sh`. A parallel Tier-3 Maestro flow
(`.maestro/handbook/NN-*.yaml`) covers navigation/tap-routing smoke for the
Expand Down
3 changes: 3 additions & 0 deletions lib/widgets/form/labeled_text_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class LabeledTextField extends StatelessWidget {
final String? initialValue;
final TextEditingController? controller;
final String? hintText;
final String? prefixText;
final TextInputType? keyboardType;
final void Function(String)? onChanged;
final String? Function(String?)? validator;
Expand All @@ -21,6 +22,7 @@ class LabeledTextField extends StatelessWidget {
this.initialValue,
this.controller,
this.hintText,
this.prefixText,
this.keyboardType,
this.onChanged,
this.validator,
Expand Down Expand Up @@ -57,6 +59,7 @@ class LabeledTextField extends StatelessWidget {
textCapitalization: textCapitalization,
decoration: InputDecoration(
hintText: hintText,
prefixText: prefixText,
enabledBorder: const OutlineInputBorder(
borderRadius: .all(.circular(8.0)),
borderSide: BorderSide(color: RealUnitColors.neutral300),
Expand Down
48 changes: 33 additions & 15 deletions lib/widgets/form/phone_number_field.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:realunit_wallet/generated/i18n.dart';
import 'package:realunit_wallet/widgets/form/dropdown_field.dart';
import 'package:realunit_wallet/widgets/form/labeled_text_field.dart';

class PhoneNumberField extends StatefulWidget {
Expand All @@ -13,26 +13,31 @@ class PhoneNumberField extends StatefulWidget {
}

class _PhoneNumberFieldState extends State<PhoneNumberField> {
final prefixes = ['+41', '+49'];
// Used only to decompose a seeded value. Input is free-form and not limited to this list.
// `+41` stays first: it is the fallback default (`prefix ??= prefixes.first`).
final prefixes = ['+41', '+49', '+43', '+423'];
String? prefix;
String? number;

@override
void initState() {
super.initState();
final value = widget.controller.value;
for (final p in prefixes) {
// Longest first. Keep this whenever the list mixes lengths: a shorter listed
// code that is a true prefix of a longer one would steal the match.
final knownPrefixes = List<String>.of(prefixes)..sort((a, b) => b.length.compareTo(a.length));
for (final p in knownPrefixes) {
if (value != null && value.startsWith(p)) {
prefix = p;
number = value.substring(p.length);
break;
}
}

// A seeded value this field cannot decompose (empty, or a dial code it does not offer) must not
// leave `prefix` null: the dropdown carries no validator, so `Form.validate()` would pass while
// `updatePhoneNumber()` silently refused to write, and the stale value would be submitted
// instead of what the user typed. Fall back to the first prefix; the number field starts empty,
// A seeded value this field cannot decompose (empty, or a dial code it does not
// recognize) must not leave `prefix` null: `updatePhoneNumber()` silently refuses
// to write when prefix is null, and the stale value would be submitted instead of
// what the user typed. Fall back to the first prefix; the number field starts empty,
// so the validator still blocks submit until it is re-entered.
prefix ??= prefixes.first;
}
Expand Down Expand Up @@ -66,16 +71,29 @@ class _PhoneNumberFieldState extends State<PhoneNumberField> {
children: [
Expanded(
flex: 3,
child: DropdownField<String>(
initialValue: prefix,
items: prefixes
.map((d) => DropdownMenuItem(value: d, child: Text(d.toString())))
.toList(),
child: LabeledTextField(
initialValue: prefix?.substring(1),
prefixText: '+',
keyboardType: .phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(3),
],
onChanged: (v) {
if (v != null) {
prefix = v;
updatePhoneNumber();
prefix = '+$v';
updatePhoneNumber();
},
hideErrorText: false,
validator: (value) {
if (value == null || value.isEmpty) {
return S.of(context).registerPhoneNumberPrefixInvalid;
}
if (!RegExp(r'^[0-9]{1,3}$').hasMatch(value)) {
return S.of(context).registerPhoneNumberPrefixFormat;
}
// Existence of the dial code is validated by the API (libphonenumber); the
// client must not gate on it — see CONTRIBUTING "the API decides".
return null;
},
),
),
Expand Down
3 changes: 1 addition & 2 deletions scripts/assemble-handbook-screenshots.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
# Assemble the 279 handbook screenshots from the visual-regression Golden
# Assemble the 278 handbook screenshots from the visual-regression Golden
# baselines. The flat `NN-name.png` output layout matches what
# docs/handbook/de/index.html links to (`<img src="../screenshots/NN-name.png">`
# — the relative path resolves to `docs/handbook/screenshots/NN-name.png`).
Expand Down Expand Up @@ -211,7 +211,6 @@ MAPPING=(
"161-kyc-registration-page-default=screens/kyc/goldens/macos/kyc_registration_page_default.png"
"162-kyc-registration-page-prefilled=screens/kyc/goldens/macos/kyc_registration_page_prefilled.png"
"163-kyc-registration-personal-step-account-type-open=screens/kyc/goldens/macos/kyc_registration_personal_step_account_type_open.png"
"164-kyc-registration-personal-step-phone-prefix-open=screens/kyc/goldens/macos/kyc_registration_personal_step_phone_prefix_open.png"
"165-kyc-registration-personal-step-validation-error=screens/kyc/goldens/macos/kyc_registration_personal_step_validation_error.png"
"166-kyc-registration-page-address-step=screens/kyc/goldens/macos/kyc_registration_page_address_step.png"
"167-kyc-registration-address-step-validation-error=screens/kyc/goldens/macos/kyc_registration_address_step_validation_error.png"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -93,24 +93,5 @@ void main() {
},
builder: buildSubject,
);

goldenTest(
'phone-prefix dropdown open — the +41 / +49 overlay menu',
fileName: 'kyc_registration_personal_step_phone_prefix_open',
constraints: phoneConstraints,
pumpBeforeTest: (tester) async {
await tester.pumpAndSettle();
// The prefix dropdown renders its selected value '+41'; the birthday
// String dropdowns show day/month/year hints instead, so this is unique.
final field = find.widgetWithText(
DropdownButtonFormField<String>,
'+41',
);
await tester.ensureVisible(field);
await tester.tap(field);
await tester.pumpAndSettle();
},
builder: buildSubject,
);
});
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions test/screens/kyc/steps/kyc_personal_data_page_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,9 @@ void main() {
testWidgets('renders every field the personal-data step submits', (tester) async {
await tester.pumpApp(buildSubject(KycPersonalDataView(url: url, initialUserData: userDataDto())));

// six placed directly (first/last name, street, house number, postcode, city) plus the one
// PhoneNumberField nests for the number input
expect(find.byType(LabeledTextField), findsNWidgets(7));
// six placed directly (first/last name, street, house number, postcode, city) plus the two
// PhoneNumberField nests for prefix and number
expect(find.byType(LabeledTextField), findsNWidgets(8));
expect(find.byType(PhoneNumberField), findsOne);
expect(find.byType(CountryField), findsOne);
expect(find.byType(FilledButton), findsOne);
Expand Down Expand Up @@ -180,7 +180,7 @@ void main() {
await tester.pumpApp(buildSubject(KycPersonalDataView(url: url, initialUserData: dto)));
await tester.pumpAndSettle();

expect(find.text('+41'), findsOne);
expect(find.text('41'), findsOne);
});

// The country lookup is fire-and-forget; without a catch a failing GET escapes as an uncaught
Expand Down
96 changes: 88 additions & 8 deletions test/widgets/form/phone_number_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,16 @@ Future<_PhoneFieldHarness> _pumpPhoneField(
return _PhoneFieldHarness(formKey: formKey, controller: controller);
}

Finder _prefixField() => find.byType(TextFormField).first;

Finder _numberField() => find.byType(TextFormField).at(1);

Future<bool> _enterAndValidate(
WidgetTester tester,
_PhoneFieldHarness harness,
String nationalNumber,
) async {
await tester.enterText(find.byType(TextFormField), nationalNumber);
await tester.enterText(_numberField(), nationalNumber);

final isValid = harness.formKey.currentState!.validate();
await tester.pump();
Expand Down Expand Up @@ -68,6 +72,20 @@ void main() {
expect(harness.controller.value, isNull);
});

testWidgets('shows the required error for an empty prefix', (tester) async {
final harness = await _pumpPhoneField(tester);

await tester.enterText(_prefixField(), '');
final isValid = harness.formKey.currentState!.validate();
await tester.pump();

expect(isValid, isFalse);
expect(
find.text(_phoneError(tester, (s) => s.registerPhoneNumberPrefixInvalid)),
findsOneWidget,
);
});

testWidgets('shows the digits-only error for non-digit input', (tester) async {
final harness = await _pumpPhoneField(tester);

Expand Down Expand Up @@ -117,18 +135,80 @@ void main() {
expect(isValid, isTrue);
});

testWidgets('accepts a non-CH/DE prefix and composes the stored number', (tester) async {
final harness = await _pumpPhoneField(tester);

await tester.enterText(_prefixField(), '43');
final isValid = await _enterAndValidate(tester, harness, '12345');

expect(harness.controller.value, '+4312345');
expect(isValid, isTrue);
});

testWidgets('switching the country prefix recomposes the stored number', (tester) async {
final harness = await _pumpPhoneField(tester, initialPhoneNumber: '+41791234567');

// Drive the prefix dropdown's onChanged directly (deterministic, no
// overlay menu): the national part is preserved and re-prefixed.
final dropdown = tester.widget<DropdownButtonFormField<String>>(
find.byType(DropdownButtonFormField<String>),
);
dropdown.onChanged!('+49');
await tester.pumpAndSettle();
await tester.enterText(_prefixField(), '49');
await tester.pump();

expect(harness.controller.value, '+49791234567');
});

testWidgets('does not accept more than 3 prefix digits', (tester) async {
final harness = await _pumpPhoneField(tester);

// LengthLimitingTextInputFormatter keeps the old value when the field is
// already at maxLength and the incoming edit is longer (collapsed
// selection). Clear first so '1234' is truncated to '123' rather than
// rejected against a 3-digit seed.
await tester.enterText(_prefixField(), '');
await tester.enterText(_prefixField(), '1234');
await tester.enterText(_numberField(), '791234567');
await tester.pump();

final prefixEditable = tester.widget<EditableText>(
find.descendant(of: _prefixField(), matching: find.byType(EditableText)),
);
expect(prefixEditable.controller.text, '123');
expect(harness.controller.value, '+123791234567');
});

testWidgets('accepts a 3-digit prefix and composes the stored number', (tester) async {
final harness = await _pumpPhoneField(tester);

await tester.enterText(_prefixField(), '423');
final isValid = await _enterAndValidate(tester, harness, '6641234567');

expect(harness.controller.value, '+4236641234567');
expect(isValid, isTrue);
});

testWidgets('decomposes a seeded +423 number without letting +43 eat it', (tester) async {
await _pumpPhoneField(tester, initialPhoneNumber: '+4236641234567');

final prefixEditable = tester.widget<EditableText>(
find.descendant(of: _prefixField(), matching: find.byType(EditableText)),
);
final numberEditable = tester.widget<EditableText>(
find.descendant(of: _numberField(), matching: find.byType(EditableText)),
);
expect(prefixEditable.controller.text, '423');
expect(numberEditable.controller.text, '6641234567');
});

testWidgets('keeps + out of the prefix field text and on prefixText', (tester) async {
final harness = await _pumpPhoneField(tester);

await tester.enterText(_prefixField(), '41');
await tester.enterText(_numberField(), '791234567');
await tester.pump();

expect(harness.controller.value, '+41791234567');
expect(find.text('+41'), findsNothing);
final prefixTextField = tester.widget<TextField>(
find.descendant(of: _prefixField(), matching: find.byType(TextField)),
);
expect(prefixTextField.decoration?.prefixText, '+');
});
});
}
Loading