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:
- There aren't constant UI updates occurring with every keyboard interaction
- 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.



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.
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.

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.



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.
signUpPageTwoPresenterDelegateTermsAndConditionsSelected
: This function opens a web view that displays terms and conditions information.signUpPageTwoPresenterDelegatePrivacyPolicySelected
: This function opens a web view that displays privacy information.signUpPageTwoPresenterDelegateContinueSelected
: This function, if itsshouldContinue
argument istrue
, will call thecreateClient
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:
searchAndPopulateSuggestions
, andretrieveFullAddress
.
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:
addressSuggestionsSelectAddressAction
,addressSuggestionsEnterAddressManuallyAction
,addressSuggestionsBackgroundTapAction
,addressSuggestionsInputTextWasUpdatedAction
,addressSuggestionsClearButtonAction
, andaddressSuggestionsOnSubmitAction
.
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:
footerButtonAction
,customerSupportSelected
,betstopLinkSelected
,termsAndConditionsLinkSelected
,privacyPolicyLinkSelected
,onMarketingConsentChanged
,formFocusChangeToUserDetail
,userDetail(valueChanged:)
,signUpPageTwoDepositLimitActionSelected
,searchForAddressAction
,formFocusChangeToAddressDetail
,addressDetail(valueChanged:)
, andfocusNextInputField
.
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 Bind
s 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
Role | Contact |
---|---|
PM | Lily Sommers |
Android Lead | Anthony Librio |
iOS Lead | Nicholas Vella |