Sign Up Flow#
In this example, we will create a sign up flow that allows users to sign up and login to their account.
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 navigationA
title
: The title of the screen, which is rendered at the top of the screenA
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.
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:
A
TextHeading
, which welcomes the userA
EmbeddedLink
with anAction
that navigates to theSIGN_UP
screenA
EmbeddedLink
with anAction
that navigates to theLOGIN
screen
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:
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 detailsA
EmbeddedLink
to theLOGIN
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 upA
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 requiredA
TextInput
field for the last name, which is requiredA
TextInput
field for the email address (the input type is set toInputType.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 toInputType.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 toInputType.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.
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 toInputType.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 toInputType.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:
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 storeA
TextSubheading
, which tells the user that they are now logged inA
Form
, which contains anOptIn
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 isVersion.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 theSTART
screen to theSIGN_UP
andLOGIN
screens, from theSIGN_UP
screen to theLOGIN
screen (and the other way around), and from theLOGIN
screen to theLOGIN_SUCCESS
screen. TheLOGIN_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 messagestoken
: The token of the WhatsApp account that we are using to send and receive messagesserver
: The Flask app that we created earlier, which will be used to register the routescallback_url
: The URL that WhatsApp will use to send us updateswebhook_endpoint
: The endpoint that WhatsApp will use to send us updatesverify_token
: Used by WhatsApp to challenge the server when we register the webhookapp_id
: The ID of the WhatsApp App, needed to register the callback URLapp_secret
: The secret of the WhatsApp App, needed to register the callback URLbusiness_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 sendmode
: The mode of the flow. We are usingFlowStatus.DRAFT
because we are still testing the flow. When we are ready to publish the flow, we can change the mode toFlowStatus.PUBLISHED
flow_action_type
: The action that will be triggered when the user clicks on the button. In this case, we are usingFlowActionType.NAVIGATE
to navigate to theSTART
screenflow_action_screen
: The name of the screen that we want to navigate to. In this case, we are usingSTART
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
: TheWhatsApp
class instanceflow
: AFlowRequest
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 (thepayload
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 settingacknowledge_errors
parameter toFalse
inon_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 messageCheck if the password and confirm password match. If they donât, we navigate again to
SIGN_UP
screen and show an error messageCheck if the password contains at least one number. If it doesnât, we navigate again to
SIGN_UP
screen and show an error messageIf 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 theLOGIN
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 messageCheck if the password is correct. If itâs not, we need to navigate again to
LOGIN
screen and show an error messageIf 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 itA more detailed
LOGIN_SUCCESS
screen, which shows the userâs name, email address and other detailsTry to adding a nice image to the
START
screen, to make it more appealingA
LOGOUT
screen, which allows the user to logout from their accountAllow the user to change their email & password
Allow the user to close the flow at any screen