Sign Up Flow#

In this example, we will create a sign up flow that allows users to sign up and login to their account.

Sign Up Flow

Think of a Flow as a collection of related screens. The screens can exchange data with each other and with your server.

A screen can be static: it can display static content that configured when the flow is created. For example, a screen can display a generic welcome message without any dynamic content, or it can display a message that will be different for each user by providing the message text when the flow is sent to the user or when it requested from the server.

Almost every aspect of a screen component can be dynamic. For example, let’s take the TextInput component which is used to collect user input (e.g name, email, password, etc.). The label, the input type, the helper text, the minimum and maximum number of characters, and whether the input is required or not, if the field is pre-filled with a value, and if the field is disabled or not, can all be dynamic.

Each Screen has

  • A id: The unique ID of the screen, which is used for navigation

  • A title: The title of the screen, which is rendered at the top of the screen

  • A layout: The layout of the screen, which contains the elements that are displayed on the screen.

  • A data: The data that the screen expects to receive. This data is used to insert content and configure the screen in order to make it dynamic.

Important thing to understand is that it doesn’t matter in which order the screens are defined, every screen is independent and has its own data.

I think it’s easier to understand this with an examples, so let’s get started.

Start Screen#

Let’s start from the START screen. This screen welcomes the user and allows them to choose if they want to sign up (create an account) or login to their existing account.

start_screen.py#
 1START = Screen(
 2    id="START",
 3    title="Home",
 4    layout=Layout(
 5        children=[
 6            TextHeading(
 7                text="Welcome to our app",
 8            ),
 9            EmbeddedLink(
10                text="Click here to sign up",
11                on_click_action=Action(
12                    name=FlowActionType.NAVIGATE,
13                    next=ActionNext(
14                        type=ActionNextType.SCREEN,
15                        name="SIGN_UP",
16                    ),
17                    payload={
18                        "first_name_initial_value": "",
19                        "last_name_initial_value": "",
20                        "email_initial_value": "",
21                        "password_initial_value": "",
22                        "confirm_password_initial_value": "",
23                    },
24                ),
25            ),
26            EmbeddedLink(
27                text="Click here to login",
28                on_click_action=Action(
29                    name=FlowActionType.NAVIGATE,
30                    next=ActionNext(
31                        type=ActionNextType.SCREEN,
32                        name="LOGIN",
33                    ),
34                    payload={
35                        "email_initial_value": "",
36                        "password_initial_value": "",
37                    },
38                ),
39            ),
40        ]
41    ),
42)

This is an example of static screen. The screen doesn’t expect to receive any data and all its components are pre-configured.

The START screen has three components:

Each EmbeddedLink has an .on_click_action with Action value, so when the user clicks on the link, the action is triggered. In this case, the action is to FlowActionType.NAVIGATE to another screen. The payload contains the data that will be passed to the navigated screen, in this case, we are passing the expected data of the SIGN_UP and LOGIN screens.

We will see how this works later on.

Sign Up Screen#

The SIGN_UP screen allows the user to sign up (create an account). Let’s take a look at the layout:

sign_up_screen.py#
 1SIGN_UP = Screen(
 2    id="SIGN_UP",
 3    title="Sign Up",
 4    data=[
 5        first_name_initial_value := ScreenData(key="first_name_initial_value", example="John"),
 6        last_name_initial_value := ScreenData(key="last_name_initial_value", example="Doe"),
 7        email_initial_value := ScreenData(key="email_initial_value", example="john.doe@gmail.com"),
 8        password_initial_value := ScreenData(key="password_initial_value", example="abc123"),
 9        confirm_password_initial_value := ScreenData(key="confirm_password_initial_value", example="abc123"),
10    ],
11    layout=Layout(
12        children=[
13            TextHeading(
14                text="Please enter your details",
15            ),
16            EmbeddedLink(
17                text="Already have an account?",
18                on_click_action=Action(
19                    name=FlowActionType.NAVIGATE,
20                    next=ActionNext(
21                        type=ActionNextType.SCREEN,
22                        name="LOGIN",
23                    ),
24                    payload={
25                        "email_initial_value": "",
26                        "password_initial_value": "",
27                    },
28                ),
29            ),
30            Form(
31                name="form",
32                children=[
33                    first_name := TextInput(
34                        name="first_name",
35                        label="First Name",
36                        input_type=InputType.TEXT,
37                        required=True,
38                        init_value=first_name_initial_value.data_key,
39                    ),
40                    last_name := TextInput(
41                        name="last_name",
42                        label="Last Name",
43                        input_type=InputType.TEXT,
44                        required=True,
45                        init_value=last_name_initial_value.data_key,
46                    ),
47                    email := TextInput(
48                        name="email",
49                        label="Email Address",
50                        input_type=InputType.EMAIL,
51                        required=True,
52                        init_value=email_initial_value.data_key,
53                    ),
54                    password := TextInput(
55                        name="password",
56                        label="Password",
57                        input_type=InputType.PASSWORD,
58                        min_chars=8,
59                        max_chars=16,
60                        helper_text="Password must contain at least one number",
61                        required=True,
62                        init_value=password_initial_value.data_key,
63                    ),
64                    confirm_password := TextInput(
65                        name="confirm_password",
66                        label="Confirm Password",
67                        input_type=InputType.PASSWORD,
68                        min_chars=8,
69                        max_chars=16,
70                        required=True,
71                        init_value=confirm_password_initial_value.data_key,
72                    ),
73                    Footer(
74                        label="Done",
75                        on_click_action=Action(
76                            name=FlowActionType.DATA_EXCHANGE,
77                            payload={
78                                "first_name": first_name.form_ref,
79                                "last_name": last_name.form_ref,
80                                "email": email.form_ref,
81                                "password": password.form_ref,
82                                "confirm_password": confirm_password.form_ref,
83                            },
84                        ),
85                    ),
86                ]
87            )
88        ]
89    )
90)

Ok, that’s a lot of code. Let’s break it down.

In this examples we are using the walrus operator (:=) to assign values to variables. This allows us to use the variables later on in the code without having to declare them outside of the layout and then assign them values later

The SIGN_UP screen expects to receive some data. In this case, we are expecting to receive some values to pre-fill the form fields.

The data of the screen is represented by the .data property. The data is a list of ScreenData objects.

Every ScreenData need to have a unique key and an example value. The example value is used to generate the appropriate JSON schema for the data. Also, we are assigning every ScreenData to a variable (inlined with the walrus operator) so that we can use them later on in the code to reference the data and “use” it in the screen (e.g. first_name_initial_value.data_key).

The layout of the SIGN_UP screen contains the following elements:

  • A TextHeading, which asks the user to enter their details

  • A EmbeddedLink to the LOGIN screen, which allows the user to login if they already have an account (the user just remembered that they already have an account)

  • A Form, which contains the form fields that the user needs to fill in to sign up

  • A Footer, which contains a button that the user can click to submit the form

The Form fields are:

  • A TextInput field for the first name, which is required

  • A TextInput field for the last name, which is required

  • A TextInput field for the email address (the input type is set to InputType.EMAIL, so that the keyboard on the user’s phone will show the @ symbol and validate the email address. Also, the input is required)

  • A TextInput field for the password (the input type is set to InputType.PASSWORD, so that the user’s password is hidden when they type it) We are also providing a helper text to tell the user that the password must contain at least one number. Also, the minimum number of characters is 8 and the maximum is 16, and the input is required)

  • A TextInput field for the confirm password (the input type is set to InputType.PASSWORD, so that the user’s password is hidden when they re-type it)

Now, every form child get assigned to a variable (inlined with the walrus operator) so that we can use them later on in the code to reference the form fields and send their “values” to the server or to another screen (e.g. first_name.form_ref).

The Footer contains a button that the user can click to submit the form. When the user clicks on the button, the Action FlowActionType.DATA_EXCHANGE is triggered. This action type allows us to send data to the server and then decide what to do next (for example, if the user is already registered, we can navigate to the LOGIN screen, or if the password and confirm password do not match, we can show an error message and ask the user to try again).

The payload of the Action of the Footer contains the data that we want to send to the server. In this case, we are sending the values of the form fields. The values can be either a DataKey or a FormRef. A DataKey is used to reference a screen’s .data items and a FormRef is used to reference Form children. Because we are using the walrus operator to assign the form fields to variables, we can use the variables to reference the form fields by using the the .form_ref property of the form field (which is more type-safe than using the FormRef with the form field’s name).

The .form_ref and .data_key properties are equivalent to the FormRef with the form field’s name and the DataKey with the screen’s data key, respectively. Infact, the .form_ref and .data_key properties are just shortcuts for the FormRef and DataKey classes.

Sign In Screen#

Ok, now to the LOGIN screen. This screen allows the user to login to their existing account.

login_screen.py#
 1LOGIN = Screen(
 2    id="LOGIN",
 3    title="Login",
 4    terminal=True,
 5    data=[
 6        email_initial_value := ScreenData(key="email_initial_value", example="john.doe@gmail.com"),
 7        password_initial_value := ScreenData(key="password_initial_value", example="abc123"),
 8    ],
 9    layout=Layout(
10        children=[
11            TextHeading(
12                text="Please enter your details"
13            ),
14            EmbeddedLink(
15                text="Don't have an account?",
16                on_click_action=Action(
17                    name=FlowActionType.NAVIGATE,
18                    next=ActionNext(
19                        type=ActionNextType.SCREEN,
20                        name="SIGN_UP",
21                    ),
22                    payload={
23                        "email_initial_value": "",
24                        "password_initial_value": "",
25                        "confirm_password_initial_value": "",
26                        "first_name_initial_value": "",
27                        "last_name_initial_value": "",
28                    },
29                ),
30            ),
31            Form(
32                name="form",
33                children=[
34                    email := TextInput(
35                        name="email",
36                        label="Email Address",
37                        input_type=InputType.EMAIL,
38                        required=True,
39                        init_value=email_initial_value.data_key,
40                    ),
41                    password := TextInput(
42                        name="password",
43                        label="Password",
44                        input_type=InputType.PASSWORD,
45                        required=True,
46                        init_value=password_initial_value.data_key,
47                    ),
48                    Footer(
49                        label="Done",
50                        on_click_action=Action(
51                            name=FlowActionType.DATA_EXCHANGE,
52                            payload={
53                                "email": email.form_ref,
54                                "password": password.form_ref,
55                            },
56                        ),
57                    ),
58                ]
59            )
60        ]
61    )
62)

This screen is very straightforward. It has two elements:

  • A TextInput field for the email address (the input type is set to InputType.EMAIL, so that the keyboard on the user’s phone will show the @ symbol and validate the email address)

  • A TextInput field for the password (the input type is set to InputType.PASSWORD, so that the user’s password is hidden when they type it)

The Footer contains a button that the user can click to submit the form. When the user clicks on the button, We are using the FlowActionType.DATA_EXCHANGE action type to send the email and password that the user entered, to the server and then decide what to do next (for example, if the user is not registered, we can navigate to the SIGN_UP screen, or if the password is incorrect, we can show an error message and ask the user to try again).

Login Success Screen#

Now, to the last screen, the LOGIN_SUCCESS screen. This screen is displayed when the user successfully logs in:

login_success_screen.py#
 1LOGIN_SUCCESS = Screen(
 2    id="LOGIN_SUCCESS",
 3    title="Success",
 4    terminal=True,
 5    layout=Layout(
 6        children=[
 7            TextHeading(
 8                text="Welcome to our store",
 9            ),
10            TextSubheading(
11                text="You are now logged in",
12            ),
13            Form(
14                name="form",
15                children=[
16                    stay_logged_in := OptIn(
17                        name="stay_logged_in",
18                        label="Stay logged in",
19                    ),
20                    Footer(
21                        label="Done",
22                        on_click_action=Action(
23                            name=FlowActionType.COMPLETE,
24                            payload={
25                                "stay_logged_in": stay_logged_in.form_ref,
26                            },
27                        ),
28                    ),
29                ]
30            )
31        ]
32    ),
33)

This screen has two elements:

  • A TextHeading, which welcomes the user to the store

  • A TextSubheading, which tells the user that they are now logged in

  • A Form, which contains an OptIn field that asks the user if they want to stay logged in

The Footer contains a button that the user can click to submit the form. The COMPLETE action is used to complete the flow. When the user clicks on the button, we are using the FlowActionType.COMPLETE action to send the value of the OptIn field to the server and then complete the flow.

This screen is the only screen that can complete the flow, that’s why we are setting the terminal property to True.

Creating the Flow#

Now, we need to wrap everything in a FlowJSON object and create the flow:

 1from pywa import utils
 2from pywa.types.flows import FlowJSON
 3
 4SIGN_UP_FLOW_JSON = FlowJSON(
 5    data_api_version=utils.Version.FLOW_DATA_API,
 6    routing_model={
 7        "START": ["SIGN_UP", "LOGIN"],
 8        "SIGN_UP": ["LOGIN"],
 9        "LOGIN": ["LOGIN_SUCCESS"],
10        "LOGIN_SUCCESS": [],
11    },
12    screens=[
13        START,
14        SIGN_UP,
15        LOGIN,
16        LOGIN_SUCCESS,
17    ]
18)

The FlowJSON object contains the following properties:

  • data_api_version: The version of the data API that we are using. We are using the latest version, which is Version.FLOW_DATA_API

  • routing_model: The routing model of the flow. This is used to define the flow’s navigation. In this case, we are using a simple routing model that allows us to navigate from the START screen to the SIGN_UP and LOGIN screens, from the SIGN_UP screen to the LOGIN screen (and the other way around), and from the LOGIN screen to the LOGIN_SUCCESS screen. The LOGIN_SUCCESS can’t navigate to any other screen.

  • screens: The screens of the flow. In this case, we are using the screens that we created earlier.

Here is all the flow code in one place:

  1from pywa import utils
  2from pywa.types.flows import (
  3    FlowJSON,
  4    Screen,
  5    ScreenData,
  6    Form,
  7    Footer,
  8    Layout,
  9    Action,
 10    ActionNext,
 11    ActionNextType,
 12    FlowActionType,
 13    FormRef,
 14    InputType,
 15    TextHeading,
 16    TextSubheading,
 17    TextInput,
 18    OptIn,
 19    EmbeddedLink,
 20)
 21
 22SIGN_UP_FLOW_JSON = FlowJSON(
 23    data_api_version=utils.Version.FLOW_DATA_API,
 24    routing_model={
 25        "START": ["SIGN_UP", "LOGIN"],
 26        "SIGN_UP": ["LOGIN"],
 27        "LOGIN": ["LOGIN_SUCCESS"],
 28        "LOGIN_SUCCESS": [],
 29    },
 30    screens=[
 31        Screen(
 32            id="START",
 33            title="Home",
 34            layout=Layout(
 35                children=[
 36                    TextHeading(
 37                        text="Welcome to our app",
 38                    ),
 39                    EmbeddedLink(
 40                        text="Click here to sign up",
 41                        on_click_action=Action(
 42                            name=FlowActionType.NAVIGATE,
 43                            next=ActionNext(
 44                                type=ActionNextType.SCREEN,
 45                                name="SIGN_UP",
 46                            ),
 47                            payload={
 48                                "first_name_initial_value": "",
 49                                "last_name_initial_value": "",
 50                                "email_initial_value": "",
 51                                "password_initial_value": "",
 52                                "confirm_password_initial_value": "",
 53                            },
 54                        ),
 55                    ),
 56                    EmbeddedLink(
 57                        text="Click here to login",
 58                        on_click_action=Action(
 59                            name=FlowActionType.NAVIGATE,
 60                            next=ActionNext(
 61                                type=ActionNextType.SCREEN,
 62                                name="LOGIN",
 63                            ),
 64                            payload={
 65                                "email_initial_value": "",
 66                                "password_initial_value": "",
 67                            },
 68                        ),
 69                    ),
 70                ]
 71            ),
 72        ),
 73        Screen(
 74            id="SIGN_UP",
 75            title="Sign Up",
 76            data=[
 77                first_name_initial_value := ScreenData(key="first_name_initial_value", example="John"),
 78                last_name_initial_value := ScreenData(key="last_name_initial_value", example="Doe"),
 79                email_initial_value := ScreenData(key="email_initial_value", example="john.doe@gmail.com"),
 80                password_initial_value := ScreenData(key="password_initial_value", example="abc123"),
 81                confirm_password_initial_value := ScreenData(key="confirm_password_initial_value", example="abc123"),
 82            ],
 83            layout=Layout(
 84                children=[
 85                    TextHeading(
 86                        text="Please enter your details",
 87                    ),
 88                    EmbeddedLink(
 89                        text="Already have an account?",
 90                        on_click_action=Action(
 91                            name=FlowActionType.NAVIGATE,
 92                            next=ActionNext(
 93                                type=ActionNextType.SCREEN,
 94                                name="LOGIN",
 95                            ),
 96                            payload={
 97                                "email_initial_value": "",
 98                                "password_initial_value": "",
 99                            },
100                        ),
101                    ),
102                    Form(
103                        name="form",
104                        children=[
105                            first_name := TextInput(
106                                name="first_name",
107                                label="First Name",
108                                input_type=InputType.TEXT,
109                                required=True,
110                                init_value=first_name_initial_value.data_key,
111                            ),
112                            last_name := TextInput(
113                                name="last_name",
114                                label="Last Name",
115                                input_type=InputType.TEXT,
116                                required=True,
117                                init_value=last_name_initial_value.data_key,
118                            ),
119                            email := TextInput(
120                                name="email",
121                                label="Email Address",
122                                input_type=InputType.EMAIL,
123                                required=True,
124                                init_value=email_initial_value.data_key,
125                            ),
126                            password := TextInput(
127                                name="password",
128                                label="Password",
129                                input_type=InputType.PASSWORD,
130                                min_chars=8,
131                                max_chars=16,
132                                helper_text="Password must contain at least one number",
133                                required=True,
134                                init_value=password_initial_value.data_key,
135                            ),
136                            confirm_password := TextInput(
137                                name="confirm_password",
138                                label="Confirm Password",
139                                input_type=InputType.PASSWORD,
140                                min_chars=8,
141                                max_chars=16,
142                                required=True,
143                                init_value=confirm_password_initial_value.data_key,
144                            ),
145                            Footer(
146                                label="Done",
147                                on_click_action=Action(
148                                    name=FlowActionType.DATA_EXCHANGE,
149                                    payload={
150                                        "first_name": first_name.form_ref,
151                                        "last_name": last_name.form_ref,
152                                        "email": email.form_ref,
153                                        "password": password.form_ref,
154                                        "confirm_password": confirm_password.form_ref,
155                                    },
156                                ),
157                            ),
158                        ]
159                    )
160                ]
161            )
162        ),
163        Screen(
164            id="LOGIN",
165            title="Login",
166            terminal=True,
167            data=[
168                email_initial_value := ScreenData(key="email_initial_value", example="john.doe@gmail.com"),
169                password_initial_value := ScreenData(key="password_initial_value", example="abc123"),
170            ],
171            layout=Layout(
172                children=[
173                    TextHeading(
174                        text="Please enter your details"
175                    ),
176                    EmbeddedLink(
177                        text="Don't have an account?",
178                        on_click_action=Action(
179                            name=FlowActionType.NAVIGATE,
180                            next=ActionNext(
181                                type=ActionNextType.SCREEN,
182                                name="SIGN_UP",
183                            ),
184                            payload={
185                                "email_initial_value": "",
186                                "password_initial_value": "",
187                                "confirm_password_initial_value": "",
188                                "first_name_initial_value": "",
189                                "last_name_initial_value": "",
190                            },
191                        ),
192                    ),
193                    Form(
194                        name="form",
195                        children=[
196                            email := TextInput(
197                                name="email",
198                                label="Email Address",
199                                input_type=InputType.EMAIL,
200                                required=True,
201                                init_value=email_initial_value.data_key,
202                            ),
203                            password := TextInput(
204                                name="password",
205                                label="Password",
206                                input_type=InputType.PASSWORD,
207                                required=True,
208                                init_value=password_initial_value.data_key,
209                            ),
210                            Footer(
211                                label="Done",
212                                on_click_action=Action(
213                                    name=FlowActionType.DATA_EXCHANGE,
214                                    payload={
215                                        "email": email.form_ref,
216                                        "password": password.form_ref,
217                                    },
218                                ),
219                            ),
220                        ]
221                    )
222                ]
223            ),
224        ),
225        Screen(
226            id="LOGIN_SUCCESS",
227            title="Success",
228            terminal=True,
229            layout=Layout(
230                children=[
231                    TextHeading(
232                        text="Welcome to our store",
233                    ),
234                    TextSubheading(
235                        text="You are now logged in",
236                    ),
237                    Form(
238                        name="form",
239                        children=[
240                            stay_logged_in := OptIn(
241                                name="stay_logged_in",
242                                label="Stay logged in",
243                            ),
244                            Footer(
245                                label="Done",
246                                on_click_action=Action(
247                                    name=FlowActionType.COMPLETE,
248                                    payload={
249                                        "stay_logged_in": stay_logged_in.form_ref,
250                                    },
251                                ),
252                            ),
253                        ]
254                    )
255                ]
256            ),
257        )
258    ]
259)

And if you want to go to the WhatsApp Flows Playground and see the flow in action, copy the equivalent JSON to the playground:

  1{
  2    "version": "3.0",
  3    "data_api_version": "3.0",
  4    "routing_model": {
  5        "START": [
  6            "SIGN_UP",
  7            "LOGIN"
  8        ],
  9        "SIGN_UP": [
 10            "LOGIN"
 11        ],
 12        "LOGIN": [
 13            "LOGIN_SUCCESS"
 14        ],
 15        "LOGIN_SUCCESS": []
 16    },
 17    "screens": [
 18        {
 19            "id": "START",
 20            "title": "Home",
 21            "layout": {
 22                "type": "SingleColumnLayout",
 23                "children": [
 24                    {
 25                        "type": "TextHeading",
 26                        "text": "Welcome to our app"
 27                    },
 28                    {
 29                        "type": "EmbeddedLink",
 30                        "text": "Click here to sign up",
 31                        "on-click-action": {
 32                            "name": "navigate",
 33                            "next": {
 34                                "type": "screen",
 35                                "name": "SIGN_UP"
 36                            },
 37                            "payload": {
 38                                "first_name_initial_value": "",
 39                                "last_name_initial_value": "",
 40                                "email_initial_value": "",
 41                                "password_initial_value": "",
 42                                "confirm_password_initial_value": ""
 43                            }
 44                        }
 45                    },
 46                    {
 47                        "type": "EmbeddedLink",
 48                        "text": "Click here to login",
 49                        "on-click-action": {
 50                            "name": "navigate",
 51                            "next": {
 52                                "type": "screen",
 53                                "name": "LOGIN"
 54                            },
 55                            "payload": {
 56                                "email_initial_value": "",
 57                                "password_initial_value": ""
 58                            }
 59                        }
 60                    }
 61                ]
 62            }
 63        },
 64        {
 65            "id": "SIGN_UP",
 66            "title": "Sign Up",
 67            "data": {
 68                "first_name_initial_value": {
 69                    "type": "string",
 70                    "__example__": "John"
 71                },
 72                "last_name_initial_value": {
 73                    "type": "string",
 74                    "__example__": "Doe"
 75                },
 76                "email_initial_value": {
 77                    "type": "string",
 78                    "__example__": "john.doe@gmail.com"
 79                },
 80                "password_initial_value": {
 81                    "type": "string",
 82                    "__example__": "abc123"
 83                },
 84                "confirm_password_initial_value": {
 85                    "type": "string",
 86                    "__example__": "abc123"
 87                }
 88            },
 89            "layout": {
 90                "type": "SingleColumnLayout",
 91                "children": [
 92                    {
 93                        "type": "TextHeading",
 94                        "text": "Please enter your details"
 95                    },
 96                    {
 97                        "type": "EmbeddedLink",
 98                        "text": "Already have an account?",
 99                        "on-click-action": {
100                            "name": "navigate",
101                            "next": {
102                                "type": "screen",
103                                "name": "LOGIN"
104                            },
105                            "payload": {
106                                "email_initial_value": "",
107                                "password_initial_value": ""
108                            }
109                        }
110                    },
111                    {
112                        "type": "Form",
113                        "name": "form",
114                        "init-values": {
115                            "first_name": "${data.first_name_initial_value}",
116                            "last_name": "${data.last_name_initial_value}",
117                            "email": "${data.email_initial_value}",
118                            "password": "${data.password_initial_value}",
119                            "confirm_password": "${data.confirm_password_initial_value}"
120                        },
121                        "children": [
122                            {
123                                "type": "TextInput",
124                                "name": "first_name",
125                                "label": "First Name",
126                                "input-type": "text",
127                                "required": true
128                            },
129                            {
130                                "type": "TextInput",
131                                "name": "last_name",
132                                "label": "Last Name",
133                                "input-type": "text",
134                                "required": true
135                            },
136                            {
137                                "type": "TextInput",
138                                "name": "email",
139                                "label": "Email Address",
140                                "input-type": "email",
141                                "required": true
142                            },
143                            {
144                                "type": "TextInput",
145                                "name": "password",
146                                "label": "Password",
147                                "input-type": "password",
148                                "required": true,
149                                "min-chars": 8,
150                                "max-chars": 16,
151                                "helper-text": "Password must contain at least one number"
152                            },
153                            {
154                                "type": "TextInput",
155                                "name": "confirm_password",
156                                "label": "Confirm Password",
157                                "input-type": "password",
158                                "required": true,
159                                "min-chars": 8,
160                                "max-chars": 16
161                            },
162                            {
163                                "type": "Footer",
164                                "label": "Done",
165                                "on-click-action": {
166                                    "name": "data_exchange",
167                                    "payload": {
168                                        "first_name": "${form.first_name}",
169                                        "last_name": "${form.last_name}",
170                                        "email": "${form.email}",
171                                        "password": "${form.password}",
172                                        "confirm_password": "${form.confirm_password}"
173                                    }
174                                }
175                            }
176                        ]
177                    }
178                ]
179            }
180        },
181        {
182            "id": "LOGIN",
183            "title": "Login",
184            "data": {
185                "email_initial_value": {
186                    "type": "string",
187                    "__example__": "john.doe@gmail.com"
188                },
189                "password_initial_value": {
190                    "type": "string",
191                    "__example__": "abc123"
192                }
193            },
194            "terminal": true,
195            "layout": {
196                "type": "SingleColumnLayout",
197                "children": [
198                    {
199                        "type": "TextHeading",
200                        "text": "Please enter your details"
201                    },
202                    {
203                        "type": "EmbeddedLink",
204                        "text": "Don't have an account?",
205                        "on-click-action": {
206                            "name": "navigate",
207                            "next": {
208                                "type": "screen",
209                                "name": "SIGN_UP"
210                            },
211                            "payload": {
212                                "email_initial_value": "",
213                                "password_initial_value": "",
214                                "confirm_password_initial_value": "",
215                                "first_name_initial_value": "",
216                                "last_name_initial_value": ""
217                            }
218                        }
219                    },
220                    {
221                        "type": "Form",
222                        "name": "form",
223                        "init-values": {
224                            "email": "${data.email_initial_value}",
225                            "password": "${data.password_initial_value}"
226                        },
227                        "children": [
228                            {
229                                "type": "TextInput",
230                                "name": "email",
231                                "label": "Email Address",
232                                "input-type": "email",
233                                "required": true
234                            },
235                            {
236                                "type": "TextInput",
237                                "name": "password",
238                                "label": "Password",
239                                "input-type": "password",
240                                "required": true
241                            },
242                            {
243                                "type": "Footer",
244                                "label": "Done",
245                                "on-click-action": {
246                                    "name": "data_exchange",
247                                    "payload": {
248                                        "email": "${form.email}",
249                                        "password": "${form.password}"
250                                    }
251                                }
252                            }
253                        ]
254                    }
255                ]
256            }
257        },
258        {
259            "id": "LOGIN_SUCCESS",
260            "title": "Success",
261            "terminal": true,
262            "layout": {
263                "type": "SingleColumnLayout",
264                "children": [
265                    {
266                        "type": "TextHeading",
267                        "text": "Welcome to our store"
268                    },
269                    {
270                        "type": "TextSubheading",
271                        "text": "You are now logged in"
272                    },
273                    {
274                        "type": "Form",
275                        "name": "form",
276                        "children": [
277                            {
278                                "type": "OptIn",
279                                "name": "stay_logged_in",
280                                "label": "Stay logged in"
281                            },
282                            {
283                                "type": "Footer",
284                                "label": "Done",
285                                "on-click-action": {
286                                    "name": "complete",
287                                    "payload": {
288                                        "stay_logged_in": "${form.stay_logged_in}"
289                                    }
290                                }
291                            }
292                        ]
293                    }
294                ]
295            }
296        }
297    ]
298}

Creating the flow is very simple using the create_flow() method:

 1from pywa import WhatsApp
 2from pywa.types.flows import FlowCategory
 3
 4wa = WhatsApp(
 5    phone_id="1234567890",
 6    token="abcdefg",
 7    business_account_id="1234567890",  # the ID of the WhatsApp Business Account
 8)
 9
10flow_id = wa.create_flow(
11    name="Sign Up Flow",
12    categories=[FlowCategory.SIGN_IN, FlowCategory.SIGN_UP],
13)

Because we are going to exchange data with our server, we need to provide endpoint URI for the flow. This is the URI that WhatsApp will use to send data to our server. We can do this by using the update_flow_metadata() method:

1wa.update_flow_metadata(
2    flow_id=flow_id,
3    endpoint_uri="https://my-server.com/sign-up-flow",
4)

This endpoint must, of course, be pointing to our server. We can use ngrok or a similar tool to expose our server to the internet.

Finally, let’s update the flow’s JSON with update_flow_json():

 1from pywa.errors import FlowUpdatingError
 2
 3try:
 4    wa.update_flow_json(
 5        flow_id=flow_id,
 6        flow_json=SIGN_UP_FLOW_JSON,
 7    )
 8    print("Flow updated successfully")
 9except FlowUpdatingError as e:
10    print("Flow updating failed")
11    print(wa.get_flow(flow_id=flow_id).validation_errors)

Storing Users#

After the flow updates successfully, we can start with our server logic. First we need a simple user repository to store the users:

 1import typing
 2
 3class UserRepository:
 4    def __init__(self):
 5        self._users = {}
 6
 7    def create(self, email: str, details: dict[str, typing.Any]):
 8        self._users[email] = details
 9
10    def get(self, email: str) -> dict[str, typing.Any] | None:
11        return self._users.get(email)
12
13    def update(self, email: str, details: dict[str, typing.Any]):
14        self._users[email] = details
15
16    def delete(self, email: str):
17        del self._users[email]
18
19    def exists(self, email: str) -> bool:
20        return email in self._users
21
22    def is_password_valid(self, email: str, password: str) -> bool:
23        return self._users[email]["password"] == password
24
25user_repository = UserRepository()  # create an instance of the user repository

Of course, in a real application, we would use a real database to store the users (and we never store the passwords in plain text…).

Sending the Flow#

To send the flow we need to initialize the WhatsApp client with some specific parameters:

 1import flask
 2from pywa import WhatsApp
 3
 4flask_app = flask.Flask(__name__)
 5
 6wa = WhatsApp(
 7    phone_id="1234567890",
 8    token="abcdefg",
 9    server=flask_app,
10    callback_url="https://my-server.com",
11    webhook_endpoint="/webhook",
12    verify_token="xyz123",
13    app_id=123,
14    app_secret="zzz",
15    business_private_key=open("private.pem").read(),
16    business_private_key_password="abc123",
17)

The WhatsApp class takes a few parameters:

  • phone_id: The phone ID of the WhatsApp account that we are using to send and receive messages

  • token: The token of the WhatsApp account that we are using to send and receive messages

  • server: The Flask app that we created earlier, which will be used to register the routes

  • callback_url: The URL that WhatsApp will use to send us updates

  • webhook_endpoint: The endpoint that WhatsApp will use to send us updates

  • verify_token: Used by WhatsApp to challenge the server when we register the webhook

  • app_id: The ID of the WhatsApp App, needed to register the callback URL

  • app_secret: The secret of the WhatsApp App, needed to register the callback URL

  • business_private_key: The private key of the WhatsApp Business Account, needed to decrypt the flow requests (see here for more info)

  • business_private_key_password: The passphrase of the private_key, if it has one

First let’s send the flow!

 1from pywa.types import FlowButton
 2from pywa.types.flows import FlowStatus, FlowActionType
 3
 4wa.send_message(
 5    to="1234567890",
 6    text="Welcome to our app! Click the button below to login or sign up",
 7    buttons=FlowButton(
 8        title="Sign Up",
 9        flow_id=flow_id,
10        flow_token="5749d4f8-4b74-464a-8405-c26b7770cc8c",
11        mode=FlowStatus.DRAFT,
12        flow_action_type=FlowActionType.NAVIGATE,
13        flow_action_screen="START",
14    )
15)

Ok, let’s break this down:

Sending a flow is very simple. We sending text (or image, video etc.) message with a FlowButton. The FlowButton contains the following properties:

  • title: The title of the button (the text that the user will see on the button)

  • flow_id: The ID of the flow that we want to send

  • mode: The mode of the flow. We are using FlowStatus.DRAFT because we are still testing the flow. When we are ready to publish the flow, we can change the mode to FlowStatus.PUBLISHED

  • flow_action_type: The action that will be triggered when the user clicks on the button. In this case, we are using FlowActionType.NAVIGATE to navigate to the START screen

  • flow_action_screen: The name of the screen that we want to navigate to. In this case, we are using START

  • flow_token: The unique token for this specific flow.

When the flow request is sent to our server, we don’t know which flow and which user the request is for. We only know the flow token. So, the flow token is used to give us some context about the flow request. We can use the flow token to identify the user and the flow. The flow token can be saved in a database or in-memory cache, and be mapped to the user ID and the flow ID (in cases you have multiple flows running at your application). And when requests are coming, you can use the flow token to identify the user and the flow and make the appropriate actions for the request.

The flow token can be also used to invalidate the flow, by raising FlowTokenNoLongerValid exception with appropriate error_message.

A good practice is to generate a unique token for each flow request. This way, we can be sure that the token is unique and that we can identify the user and the flow. You can use the uuid module to generate a unique token:

1import uuid
2
3flow_token = str(uuid.uuid4())

After we create the WhatsApp instance and we send the flow, we can start listening to flow requests:

1from pywa.types.flows import FlowRequest, FlowResponse
2
3@wa.on_flow_request("/sign-up-flow")
4def on_sign_up_request(_: WhatsApp, flow: FlowRequest) -> FlowResponse | None:
5    if flow.has_error:
6        logging.error("Flow request has error: %s", flow.data)
7        return
8
9    ...

The on_flow_request() decorator takes the endpoint URI as a parameter. This is the endpoint that we provided when we updated the flow’s metadata. So if the endpoint URI is https://my-server.com/sign-up-flow, then the endpoint URI that we are listening to is /sign-up-flow.

Yes, you can point multiple flows to the same endpoint URI. But then you need to find a way to identify the flow by the flow token. I recommend creating a unique endpoint URI for each flow.

Our on_sign_up_request calback function takes two parameters:

  • wa: The WhatsApp class instance

  • flow: A FlowRequest object, which contains the flow request data

The flow request contains the following properties:

  • version: The version of the flow data API that the flow request is using (you should use thisn version in the response)

  • flow_token: The token of the flow (the same token that we provided when we sent the flow)

  • action: The action type that was triggered the request. FlowActionType.DATA_EXCHANGE in our case.

  • screen: The name of the screen that the user is currently on (We have two screens with data exchange actions, so we need to know which screen the user is currently on)

  • data: The data that the action sent to the server (the payload property of the action)

In the top of the function, we are checking if the flow request has an error. If it does, we are logging the error and returning.

By default, if the flow has error, pywa will ignore the callback return value and will acknowledge the error. This behavior can be changed by setting acknowledge_errors parameter to False in on_flow_request decorator.

Handling Sign Up Flow Requests#

Now, let’s handle the flow request. we can handle all the screens in one code block but for the sake of simplicity, we will handle each screen separately:

 1def handle_signup_screen(request: FlowRequest) -> FlowResponse:
 2
 3    if user_repository.exists(request.data["email"]):
 4        return FlowResponse(
 5            version=request.version,
 6            screen="LOGIN",
 7            error_message="You are already registered. Please login",
 8            data={
 9                "email_initial_value": request.data["email"],
10                "password_initial_value": request.data["password"],
11            },
12        )
13    elif request.data["password"] != request.data["confirm_password"]:
14        return FlowResponse(
15            version=request.version,
16            screen=request.screen,
17            error_message="Passwords do not match",
18            data={
19                "first_name_initial_value": request.data["first_name"],
20                "last_name_initial_value": request.data["last_name"],
21                "email_initial_value": request.data["email"],
22                "password_initial_value": "",
23                "confirm_password_initial_value": "",
24            },
25        )
26    elif not any(char.isdigit() for char in request.data["password"]):
27        return FlowResponse(
28            version=request.version,
29            screen=request.screen,
30            error_message="Password must contain at least one number",
31            data={
32                "first_name_initial_value": request.data["first_name"],
33                "last_name_initial_value": request.data["last_name"],
34                "email_initial_value": request.data["email"],
35                "password_initial_value": "",
36                "confirm_password_initial_value": "",
37            },
38        )
39    else:
40        user_repository.create(request.data["email"], request.data)
41        return FlowResponse(
42            version=request.version,
43            screen="LOGIN",
44            data={
45                "email_initial_value": request.data["email"],
46                "password_initial_value": "",
47            },
48        )

So, what’s going on here?

This function handles the SIGN_UP screen. We need to check a few things:

  • Check if the user is already registered. If they are, we need to navigate to the LOGIN screen and show an error message

  • Check if the password and confirm password match. If they don’t, we navigate again to SIGN_UP screen and show an error message

  • Check if the password contains at least one number. If it doesn’t, we navigate again to SIGN_UP screen and show an error message

  • If everything is ok, we create the user and navigate to the LOGIN screen (with the email address already filled in 😋)

    Now you understand why SIGN_UP screen get’s initial values? because we don’t want the user to re-enter the data again if there is an error. From the same reason, LOGIN screen get’s initial values too, so when the sign up succeeds, the user will be navigated to the LOGIN screen with the email address already filled in.

Handling Login Flow Requests#

Now, let’s handle the LOGIN screen:

 1def handle_login_screen(request: FlowRequest) -> FlowResponse:
 2
 3    if not user_repository.exists(request.data["email"]):
 4        return FlowResponse(
 5            version=request.version,
 6            screen="SIGN_UP",
 7            error_message="You are not registered. Please sign up",
 8            data={
 9                "first_name_initial_value": "",
10                "last_name_initial_value": "",
11                "email_initial_value": request.data["email"],
12                "password_initial_value": "",
13                "confirm_password_initial_value": "",
14            },
15        )
16    elif not user_repository.is_password_valid(request.data["email"], request.data["password"]):
17        return FlowResponse(
18            version=request.version,
19            screen=request.screen,
20            error_message="Incorrect password",
21            data={
22                "email_initial_value": request.data["email"],
23                "password_initial_value": "",
24            },
25        )
26    else:
27        return FlowResponse(
28            version=request.version,
29            screen="LOGIN_SUCCESS",
30            data={},
31        )

The LOGIN screen is very similar to the SIGN_UP screen. We need to check a few things:

  • Check if the user is registered. If they are not, we need to navigate to the SIGN_UP screen and show an error message

  • Check if the password is correct. If it’s not, we need to navigate again to LOGIN screen and show an error message

  • If everything is ok, we navigate to the LOGIN_SUCCESS screen

Handling the Flow Requests#

Let’s modify out on_sign_up_request callback function to handle the SIGN_UP and LOGIN screens:

 1@wa.on_flow_request("/sign-up-flow")
 2def on_sign_up_request(_: WhatsApp, flow: FlowRequest) -> FlowResponse | None:
 3    if flow.has_error:
 4        logging.error("Flow request has error: %s", flow.data)
 5        return
 6
 7    if flow.screen == "SIGN_UP":
 8        return handle_signup_screen(flow)
 9    elif flow.screen == "LOGIN":
10        return handle_login_screen(flow)

Handling Flow Completion#

The LOGIN_SUCCESS scrren completes the flow, so we don’t need to do anything here. instead we need to handle the flow completion:

1@wa.on_flow_completion()
2def handle_flow_completion(_: WhatsApp, flow: FlowCompletion):
3    print("Flow completed successfully")
4    print(flow.token)
5    print(flow.response)

Now, in a real application, this is the time to mark the user as logged in and allow them to perform actions in their account. You can also implement some kind of session management, so that the user will stay logged in for a certain amount of time and then require them to login again.

Running the Server#

The last thing that we need to do is run the server:

1if __name__ == "__main__":
2    flask_app.run()

What’s Next?#

Now that you know how to create and send a flow, you can try to add the following features to the flow:

  • A FORGOT_PASSWORD screen, which allows the user to reset their password if they forgot it

  • A more detailed LOGIN_SUCCESS screen, which shows the user’s name, email address and other details

  • Try to adding a nice image to the START screen, to make it more appealing

  • A LOGOUT screen, which allows the user to logout from their account

  • Allow the user to change their email & password

  • Allow the user to close the flow at any screen