Skip to main content

Join Form

Overview

This page describes the form that a new customer must complete in order to create an account with an Entain brand. The information on this page will apply to all brands, for anything region specific see the below:

Validation

Almost every field in the form utilises frontend validation to ensure that the details entered are both correct for the users' sake, and so the app isn't submitting data to the database that is either incomplete or in the incorrect format.

Although implementations will vary between platforms, both follow the same high level pattern.

Every field always exists in one of four states:

  • Valid - the field has been successfully validated.
  • Invalid - the field has failed it's validations, an error message should be displayed indicating why the field failed.
  • Error - an error occurred when attempting to validate the input, most likely because an API call failed.
  • Unknown - validations have yet to be run on this field.

Upon form initialisation, every field is set as unknown, unless the field is prefilled or it is an optional field, in which case it gets sets to valid.

A form input field gets validated when the user de-focuses from the field. The idea there is so that:

  1. There aren't constant UI updates occurring with every keyboard interaction
  2. The user doesn't enter all their details and hit submit only to realise that they need to go back and fix fields that are invalid.

This way, the user can enter their email, attempt to move on, if the email in invalid then they're alerted straight away that they need to fix it. Once they've entered 'valid' input, the progress bar will increment and the user will know that they are actually making progress.

Join FormJoin FormJoin Form

When the user submits a page, the app checks that all the fields have been validated before continuing. It does this by iterating through every field, top to bottom, and checking the field's state. If the field is:

  • Valid - then next field is checked
  • Invalid - then the app requests focus and scrolls to the invalid field, the iteration also ends
  • Error - the system alert is displayed and the iteration ends
  • Unknown - validations for this field are run, which will set the field to one of the other three states, and then the relevant action is taken based on the new state.
    • This condition covers the use cases where the user has forgot to fill in a required field, or they have filled in a field but never de-focused - which is the trigger for validations to be run.

Loading State

When the call to action button is pressed on any page in the form, the page will enter a "loading state". During this state, the user's information is being validated and submitted, and so the user shouldn't be allowed to change their details until the process is finished or cancelled.

While in this state, the user can't interact with the screen with the exception of the app bar, most of the screen becomes greyed out to indicate touch is disabled, and text on the call to action button gets replaced with a loading spinner.

Join Form

For the final page of the form, the state will be visible for at least a few seconds as the app needs to validate the details locally, attempt to create the client and possibly perform other time consuming tasks, depending on the result of the client creation.

For the other pages, this state only remains while the input gets validated. Unless one of the check API call needs to run again, all of these validations will just be happening on the client side, and hence the loading screen should be hardly visible.

System Alert

The system alert dialog will be shown when the user attempts to continue to the next page or submit the form when one of the fields is in an 'error' state. This should only be possible when one of the check API calls returns an error, although it is also possible if one of the client side validation calls fail.

Join Form

There are other situations in the NZ sign up flow where the user may see an alert dialog, for more information see NZ Join Form - System Alert

Integrations

Feature Flags

  • login-native: It is used to redirect the user using native or react-native Login if the user already exists.

Inputs

REST: GET /v2/client/check-username

  • Used to check if a username is available for signup.

Harmony Address Lookup API

  • Used to autocomplete the address entered by the user.

iOS

FormViewModel

The captured user input is stored in the FormViewModel. This object is passed between each screen via parameters on the route. It originates in either the Welcome Screen or Sign Up Page 1 depending on the set feature flag. Each presenter will call initEntainFormViewModelKinds() in its init() to assign a view model to an input field if one doesn't already exist.

Email is a special case as it appears across both welcome and page 1 screens with a different title field.

As each input field can be a select or text input element, we wrap input view models in an EntainFormViewModelKind enum. Each case's associated data takes the view model that's related to it. A convenience method to easily access each view model is set up on FormViewModel.

FormViewModel.UserDetail

All the top-level fields of the form are defined by the enum FormViewModel.UserDetail. This enum allows us to:

  • Set whether or not a field is required.
  • Set whether or not a field can focus.
  • Set input limits
  • Set formatted values
  • Set autocapitalization values.
  • Look up values from the FormViewModel to validate.

FormValidation

The object responsible for validating user input on both the client and server sides is FormValidation. Each enum case in FormViewModel.UserDetail is mapped to a method that validates its input. These methods are marked async as some fields require server-side checks as listed above.

Each field will return a FormValidation.Result that can either be .valid or .invalid([rules]). FormValidation.Result contains all the error messages that are displayed to the user as well as all the rules that need to be validated. The FormTransformer will use this result to determine the correct state of the form field in question.

Each field's input is validated on de-focus. The result of the validation of the field is stored in var validationResults on the FormViewModel.

FormValidation has convenience methods to validate a single field, page, or the whole form.

Two fields have validation specific to their children: Deposit Limit and Address. For these two fields to be valid, their child fields must be valid if present.

Paging

SignUpPaging is a protocol that each page conforms to. The order of each page's field items is listed in this file should it need to be adjusted.

A page element is a FormViewModel.Row. A row can either be a single column or span multiple columns. The only multi-column row is the title and first name. The SignUpFormView uses these rows to build itself.

Input Focus

Keyboard & Toolbar

When a user focuses on a form input, a functional Toolbar will appear above the native keyboard. This Toolbar enhances the form navigation experience by allowing users to seamlessly move to the next input field without manually de-focusing the current one. The Toolbar will display an action button labeled either Next or Done, depending on whether the current field is the last focus-able element in the form. Tapping Done will automatically close the keyboard and un-focus the form.

The Toolbar also complements the native keyboard’s submit button, which may be labeled Next or Continue instead of Done. Since the native submit button might not always be available, particularly in cases of numerical input, the Toolbar ensures users can still efficiently navigate between input fields.

Next Form input focusDone Form input focusNumerical Form input focus

Child Focus

The sign up form includes instances where additional input fields are available but initially hidden from view. These fields only become visible when the user performs a specific action to expand the form. For example, selecting the "Enter Manually" option will reveal the manual address input fields, and choosing to set a deposit limit will display the corresponding input area.

Focus Logic

Each sign up presenter conforms to SignUpPaging and is responsible for setting up the order of the input fields on each page. This ordering is crucial for the focus management logic, which determines the next input field to focus on when the user taps Next, Done, or Continue. The FormView handles the main SwiftUI implementation, specifically the ToolbarItemGroup that displays the toolbar above the keyboard. Each form input within the FormView has a submitLabel, which is dynamically set based on the SignUpPaging form rows which determines the label Next or Done. Additionally, each presenter includes a focusNextInputField() function, which is called by the FormView and any child views that need to move the focus to the next input field. The focusNextInputField() will be invoked either by the input form onSubmit action or the Toolbar button action.

The core logic for determining the next input focus is implemented in FormViewModel.focusNextInputField(..SignUpPaging). This function identifies which form input should receive focus next by first checking if any child forms have active focus through FormViewModel.childFormFocus. Each child view has its own ViewModel that manages focus within that view, determining the next input field, such as with focusNextManualAddressFormFieldInput. When a child form reaches the end of its inputs, it returns the focus to the main form, where focusRootFormInput determines the next root input field to receive focus. For certain input fields, like drop-downs e.g administrative area and title, focus may not be applicable. To address this, we've established a pattern to determine when allowsFocus should be applied.

Page 2

Most of the code related to page 2 is in the PresentationLayer/Sources/SignUpUI/Screens/PageTwo directory. The coordinator and an extension on it that deals with the createClient request are in the PresentationLayer/Sources/SignUpUI directory.

SignUpPageTwoCoordinator

This is the entry point to page 2 of the sign up flow.

The route to page 2 is expected to have a parameter which contains a FormViewModel, so that data entered on previous screens can be carried through and submitted to the API when the user presses the Sign Up button at the bottom of the form.

This class conforms to the SignUpPageTwoPresenterDelegate protocol, which allows the presenter to inform it of actions the user takes. Most of the functions in this protocol are only for analytics purposes, but three of them do more than just send analytics events.

  1. signUpPageTwoPresenterDelegateTermsAndConditionsSelected: This function opens a web view that displays terms and conditions information.
  2. signUpPageTwoPresenterDelegatePrivacyPolicySelected: This function opens a web view that displays privacy information.
  3. signUpPageTwoPresenterDelegateContinueSelected: This function, if its shouldContinue argument is true, will call the createClient function defined in the extension described below.

SignUpPageTwoCoordinator+CreateClient

The only non-private function on this extension is createClient. This function attempts to build a CreateUserModel and use it in a call to the createClient function of SignUpInteractor. The function then uses private helper functions to handle the success or failure of that API call, navigating the user to the appropriate screen, or showing them an alert if some kind of error occurred.

SignUpAddressLookup

This struct provides an interface between AddressLookupInteractor and SignUpPageTwoAddressHandler that handles the logic for updating the address suggestions, and the address form view model based on a selected address suggestion. It has two non-private functions:

  1. searchAndPopulateSuggestions, and
  2. retrieveFullAddress.

searchAndPopulateSuggestions calls the findAddress function of AddressLookupInteractor, and handles the result. If it is successful, a list of dress suggestions is set on the form view model, and displayed to the user.

retrieveFullAddress calls the retrieveAddress function of AddressLookupInteractor with a Place ID from an address suggestion, and updates the address form view model with the full information for the selected address suggestion.

SignUpPageTwoAddressHandler

This struct implements AddressSuggestionsActions, which is a protocol that defines a number of functions necessary to respond to actions the user takes regarding the address search field and suggestions list. It updates state in the form and address form view models in response to user actions, and acts and an interface to SignUpAddressLookup. The protocol contains the following functions: protocol:

  1. addressSuggestionsSelectAddressAction,
  2. addressSuggestionsEnterAddressManuallyAction,
  3. addressSuggestionsBackgroundTapAction,
  4. addressSuggestionsInputTextWasUpdatedAction,
  5. addressSuggestionsClearButtonAction, and
  6. addressSuggestionsOnSubmitAction.

addressSuggestionsSelectAddressAction is called when the user taps an address suggestion in the suggestion list. It causes the selected suggestion to become the text in the address search field, and for the full details of that address to be retrieved based on its Place ID.

addressSuggestionsEnterAddressManuallyAction is called when the user taps a button to switch to manual address entry mode. If a suggested address has been selected, the details of that address will pre-fill the manual address form fields.

addressSuggestionsBackgroundTapAction is called when the user taps anywhere on the screen that isn’t the address suggestions list while the list is visible. It closes the suggestions list.

addressSuggestionsInputTextWasUpdatedAction is called when the user changes the text in the address search field. It causes a new search to be performed after a slight delay, if there are more than 3 characters in the field.

addressSuggestionsClearButtonAction is called when the user taps the clear button in the address search field, and removes and selected address info from the view models and clears the input field.

addressSuggestionsOnSubmitAction is called when the user presses return with the address search field focused, and triggers a search for the input in the address search field.

SignUpPageTwoPresenter

This is where most of the logic for page two of the sign-up form is located. The public interface used by SignUpPageTwoView is a protocol called SignUpPageTwoPresenting. It contains the following functions:

  1. footerButtonAction,
  2. customerSupportSelected,
  3. betstopLinkSelected,
  4. termsAndConditionsLinkSelected,
  5. privacyPolicyLinkSelected,
  6. onMarketingConsentChanged,
  7. formFocusChangeToUserDetail,
  8. userDetail(valueChanged:),
  9. signUpPageTwoDepositLimitActionSelected,
  10. searchForAddressAction,
  11. formFocusChangeToAddressDetail,
  12. addressDetail(valueChanged:), and
  13. focusNextInputField.

footerButtonAction is called when the user taps the Sign Up button at the bottom of the form. It disables the form, validates all fields, and attempts to send the create user request if all required fields are valid.

customerSupportSelected is called when the user taps the customer support phone number in the footer. It informs the delegate about the tap for analytics tracking.

betstopLinkSelected is called when the user taps the Betstop link in the footer. It informs the delegate about the tap for analytics tracking.

termsAndConditionsLinkSelected is called when the user taps either the terms and conditions or privacy policy link the footer. It informs the delegate so that it can route to the appropriate screen.

privacyPolicyLinkSelected is called when the user taps the privacy policy link in the footer. It informs the delegate about the tap for analytics tracking.

onMarketingConsentChanged is called when the user taps the marketing consent checkbox, and it records the latest value of the checkbox.

formFocusChangeToUserDetail is called when the user changes which field is focused. After a field has been de-focused, it is validated. The delegate is also informed of field focus changes for analytics tracking.

userDetail(valueChanged:) is called when the user updates the value of a form field. It causes the validation state for the changed field to be reset.

signUpPageTwoDepositLimitActionSelected is called when the user changes the deposit limit, to record the deposit limit settings they choose.

searchForAddressAction is called when the user taps the search key while the address search field is focused. It ensures the form is not in manual address entry mode, and updates the progress bar. It does not explicitly start a search, because a search will already have been started after any change to the search field the user makes, as long the query is more than 3 characters long.

formFocusChangeToAddressDetail is called when a field is de-focused in the manual address form. It causes the field that was switched away from to be validated, and keeps track of the new field that was focused.

addressDetail(valueChanged:) is called whenever the user updates the value of a field in the address form. It resets the validation state of the field.

focusNextInputField is called when the user taps the Next button in the toolbar above the keyboard. It calls a function of the same name on the form view model.

SignUpPageTwoView

This struct mostly creates a number of Binds that are passed to FormView.

Troubleshooting

Android

No error message

If you've entered input that you expect to fail a field's validation rules but you're not seeing an error message, ensure that you're mapping the correct FieldValidationError object to an error message string in the relevant composable.

FocusRequester is not initialized.

If you're encountering a runtime when attempting to request focus on a field, ensure that the field is in composition before calling requestFocus(). Because a LazyColumn() is the parent of all the input fields, any field that isn't currently on screen won't be rendered, so attempting request focus on an offscreen field will not work.

Ensure that animateScrollToItem is being called on the relevant field before any requestFocus call is made.

iOS

Validation Rules - Do I write a new rule in the presentation or domain layer?

The validation rules in the domain layer are more general in scope and can be adjusted to reflect more business logic rules. For example, the domain layer has a LengthValidation rule where you can specify a range that the input length must be to result .valid.

A rule in the presentation layer might consume with LengthValidation in a local variable and call it minimum8CharsValidation.

So if it's something specific, it probably belongs in the presentation layer. If it can be repurposed and describes many different rules, then it could belong in the domain layer.

How do I change the order of a field item on a page?

Head to SignUpPaging and locate the page. Add, remove, and move items there.

How to format text input

Each EntainFormInputField exposes a .onChange handler. If you need to modify or format the input, it's best to handle this in the bind. The FormViewModel has a method formattedValue(_ value: String) that will map to each of the fields. The date of birth and mobile fields are two that support custom formatting. The other user details format is based on input length.

Erratic scrolling when switching keyboard types

A known bug in iOS causes the form to scroll up (form fields move down the screen) when the keyboard type changes to numberpad. This can lead to a field becoming hidden behind the keyboard just as it gains focus.

A tempting work around is to have the form ignore the keyboard safe area and observe the appearance/disappearance of the keyboard to adjust the layout of the form, manually allowing room for the keyboard. A decision has been made, however, to rule out this approach due to the inherent fragility of relying on UIKit notifications to update a SwiftUI view. There are also concerns around this working in all contexts (e.g. within a window on an iPad with a split keyboard).

The accepted work around, so far, is to scroll the field back into position after a short delay.

Resources

RoleContact
PMLily Sommers
Android LeadAnthony Librio
iOS LeadNicholas Vella