Skip to content

Commit

Permalink
Add config login code step
Browse files Browse the repository at this point in the history
- Enter login code from the login email
- User is logged in on the client_id
- Crisp user id is saved in the config entry
  • Loading branch information
NLthijs48 committed Apr 17, 2024
1 parent 5d1f9a4 commit de16719
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 29 deletions.
10 changes: 9 additions & 1 deletion custom_components/crisp/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ class CrispApiClientAuthenticationError(CrispApiClientError):


# TODO: country enum?
# TODO: offer option to override base url? different per country
class CrispApiClient:
"""Crisp API client."""

Expand Down Expand Up @@ -75,6 +74,15 @@ async def request_login_code(self, email: str, country: str) -> any:
method="post", path="/user/login", data={"email": email, "country": country}
)

async def login(self, email: str, country: str, login_code: str) -> any:
"""Login a user using the login code retreived from the email sent by request_login_code."""

return await self._api_wrapper(
method="post",
path="/user/login",
data={"email": email, "country": country, "code": login_code},
)

async def _api_wrapper(
self,
method: str,
Expand Down
106 changes: 80 additions & 26 deletions custom_components/crisp/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_TOKEN
from homeassistant.const import CONF_EMAIL, CONF_TOKEN, CONF_CODE, CONF_COUNTRY_CODE
from homeassistant.helpers import selector
from homeassistant.helpers.aiohttp_client import async_create_clientsession

Expand All @@ -31,18 +31,29 @@ async def async_step_user(
self,
info: dict | None = None,
) -> config_entries.FlowResult:
"""Handle a flow initialized by the user: initial setup."""
errors = {}
# TODO: validate email? is there some function for that?
"""Handle a flow initialized by the user: enter email of Crisp user."""

errors: dict[str, str] = {}
if info is not None:
try:
info[CONF_TOKEN] = CrispApiClient.generate_client_id()
# Generate client_id
client_id = CrispApiClient.generate_client_id()
info[CONF_TOKEN] = client_id

# TODO: ask country from the user
country = "nl"
info[CONF_COUNTRY_CODE] = country

self.crisp_client = CrispApiClient(
session=async_create_clientsession(self.hass),
client_id=client_id,
)

# Request a login code for the user belonging to the email
response = await self.request_login_email(
client_id=info[CONF_TOKEN], email=info[CONF_EMAIL]
response = await self.crisp_client.request_login_code(
email=info[CONF_EMAIL], country=country
)
_LOGGER.debug("request_login_code response: ", response)
except CrispApiClientAuthenticationError as exception:
_LOGGER.warning(exception)
errors["base"] = "auth"
Expand All @@ -60,17 +71,11 @@ async def async_step_user(
# TODO: finish the flow without requesting login, use user id directly
errors["base"] = "Already logged in"
else:
# TODO: show login code form
# Set unique id of this config flow to the Crisp user id
# await self.async_set_unique_id(user_id)
# Ensure config flow can only be done once for this email
# self._abort_if_unique_id_configured()
# Save the email/client_id
self.user_info = info

# All good, create config entry
return self.async_create_entry(
title=info[CONF_EMAIL],
data=info,
)
# Show the login code step
return await self.async_step_login()

# Show errors in the form
return self.async_show_form(
Expand All @@ -88,16 +93,65 @@ async def async_step_user(
}
),
errors=errors,
last_step=False,
)

async def request_login_email(self, client_id: str, email: str) -> any:
"""Request a login code for the Crisp user that belongs to the given email address."""
async def async_step_login(self, info: dict | None = None):
"""Second step of the setup; enter login code for the Crisp user."""

errors: dict[str, str] = {}
if info is not None:
try:
# Try to login using the provided code
response = await self.crisp_client.login(
email=self.user_info[CONF_EMAIL],
country=self.user_info[CONF_COUNTRY_CODE],
login_code=info[CONF_CODE],
)
_LOGGER.debug("login response: ", response)
except CrispApiClientAuthenticationError as exception:
_LOGGER.warning(exception)
errors["base"] = "auth"
except CrispApiClientCommunicationError as exception:
_LOGGER.error(exception)
errors["base"] = "connection"
except CrispApiClientError as exception:
_LOGGER.exception(exception)
errors["base"] = "unknown"
else:
if "error" in response:
# These errors are not super human-readable, could attempt to map some of them to better text
errors[CONF_CODE] = response.error
elif "id" not in response:
# Did not get a user id back as confirmation, something went wrong
errors["base"] = "Unknown error, could not login"
else:
self.user_info["user_id"] = user_id = response["id"]

# Set unique id of this config flow to the Crisp user id (more stable than email, which can be changed)
await self.async_set_unique_id(user_id)
# Ensure config flow can only be done once for this Crisp user id
self._abort_if_unique_id_configured()

# All good, create config entry
return self.async_create_entry(
title=self.user_info[CONF_EMAIL],
data=self.user_info,
)

# TODO: ask for country code from the user
client = CrispApiClient(
session=async_create_clientsession(self.hass),
client_id=client_id,
return self.async_show_form(
step_id="login",
data_schema=vol.Schema(
{
vol.Required(
CONF_CODE,
default=(info or {}).get(CONF_CODE),
): selector.TextSelector(
selector.TextSelectorConfig(
type=selector.TextSelectorType.TEXT
),
),
}
),
errors=errors,
)
response = await client.request_login_code(email=email, country="nl")
_LOGGER.debug(response)
return response
10 changes: 8 additions & 2 deletions custom_components/crisp/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@
"data": {
"email": "Email"
}
},
"login": {
"description": "Enter the login code you received from Crisp on your email address.",
"data": {
"code": "Login code"
}
}
},
"abort": {
"already_configured": "Crisp account with that email is already configured."
"already_configured": "That Crisp user is already configured."
},
"error": {
"auth": "Username/Password is wrong.",
"auth": "Email is wrong.",
"connection": "Unable to connect to the server.",
"unknown": "Unknown error occurred."
}
Expand Down

0 comments on commit de16719

Please sign in to comment.