diff --git a/CHANGELOG.md b/CHANGELOG.md
index bce86a1..8d043aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -112,6 +112,12 @@
- Fix an error with URL generation for authenticated requests.
- Fix error handling for IdentityServer4 provider.
+## 1.0.35 - 2024-10-20
+
+### Added
+- Add Xero provider.
+- Add CA domain to Zoho provider.
+
## 1.0.34 - 2024-09-13
### Fixed
diff --git a/src/clients/xero/provider/Xero.php b/src/clients/xero/provider/Xero.php
new file mode 100644
index 0000000..8397741
--- /dev/null
+++ b/src/clients/xero/provider/Xero.php
@@ -0,0 +1,180 @@
+getAuthenticatedRequest(
+ self::METHOD_GET,
+ $this->getTenantsUrl($params),
+ $token
+ );
+
+ $response = $this->getParsedResponse($request);
+ $tenants = [];
+
+ foreach ($response as $tenantData) {
+ $tenants[] = XeroTenant::fromArray($tenantData);
+ }
+
+ return $tenants;
+ }
+
+ /**
+ * @param AccessTokenInterface $token
+ * @param $connectionId
+ * @return mixed
+ * @throws \League\OAuth2\Client\Provider\Exception\IdentityProviderException
+ */
+ public function disconnect(AccessTokenInterface $token, $connectionId)
+ {
+ $url = sprintf('%s/%s', $this->getTenantsUrl(), $connectionId);
+
+ $request = $this->getAuthenticatedRequest(self::METHOD_DELETE, $url, $token);
+
+ $response = $this->getParsedResponse($request);
+ return $response;
+ }
+
+
+ /**
+ * Returns the URL for requesting the resource owner's details.
+ *
+ * @param AccessToken $token
+ *
+ * @return string
+ */
+ public function getResourceOwnerDetailsUrl(AccessToken $token)
+ {
+ //This does not exist as it comes down in the JWT
+ return '';
+ }
+
+ /**
+ * @param AccessToken $token
+ * @return XeroResourceOwner
+ */
+ public function getResourceOwner(AccessToken $token)
+ {
+ return XeroResourceOwner::fromJWT($token->getValues()['id_token']);
+ }
+
+
+ /**
+ * Checks a provider response for errors.
+ *
+ * @param ResponseInterface $response
+ * @param array|string $data Parsed response data
+ *
+ * @throws \Calcinai\OAuth2\Client\Provider\Exception\XeroProviderException
+ */
+ protected function checkResponse(ResponseInterface $response, $data)
+ {
+ if ($response->getStatusCode() >= 400) {
+ throw new XeroProviderException(
+ isset($data['error']) ? $data['error'] : $response->getReasonPhrase(),
+ $response->getStatusCode(),
+ $response
+ );
+ }
+ }
+
+ /**
+ * @return array
+ */
+ protected function getDefaultScopes()
+ {
+ return ['openid email profile'];
+ }
+
+ /**
+ * Returns the string that should be used to separate scopes when building
+ * the URL for requesting an access token.
+ *
+ * @return string Scope separator, defaults to ' '
+ */
+ protected function getScopeSeparator()
+ {
+ return ' ';
+ }
+
+ /**
+ * Generates a resource owner object from a successful resource owner
+ * details request.
+ *
+ * @param array $response
+ * @param AccessToken $token
+ * @return void|ResourceOwnerInterface
+ */
+ protected function createResourceOwner(array $response, AccessToken $token)
+ {
+ // This does nothing as we get the resource owner from the token itself, don't need to make a request to get it.
+ }
+
+ /**
+ * @param mixed|null $token
+ * @return array
+ */
+ protected function getAuthorizationHeaders($token = null)
+ {
+ return [
+ 'Authorization' => 'Bearer ' . $token->getToken()
+ ];
+ }
+}
diff --git a/src/clients/xero/provider/XeroPkce.php b/src/clients/xero/provider/XeroPkce.php
new file mode 100644
index 0000000..646c267
--- /dev/null
+++ b/src/clients/xero/provider/XeroPkce.php
@@ -0,0 +1,21 @@
+xero_userid = $decoded->xero_userid;
+ $self->preferred_username = $decoded->preferred_username;
+ $self->email = $decoded->email;
+ $self->given_name = $decoded->given_name;
+ $self->family_name = $decoded->family_name;
+
+ return $self;
+ }
+}
diff --git a/src/clients/xero/provider/XeroTenant.php b/src/clients/xero/provider/XeroTenant.php
new file mode 100644
index 0000000..7d48aea
--- /dev/null
+++ b/src/clients/xero/provider/XeroTenant.php
@@ -0,0 +1,61 @@
+id = $data['id'];
+ $self->authEventId = $data['authEventId'];
+ $self->tenantId = $data['tenantId'];
+ $self->tenantType = $data['tenantType'];
+ $self->tenantName = $data['tenantName'];
+ $self->createdDateUtc = new \DateTime($data['createdDateUtc']);
+ $self->updatedDateUtc = isset($data['updatedDateUtc']) ? new \DateTime($data['updatedDateUtc']) : null;
+
+ return $self;
+ }
+}
diff --git a/src/clients/xero/provider/exception/XeroProviderException.php b/src/clients/xero/provider/exception/XeroProviderException.php
new file mode 100644
index 0000000..7ef2136
--- /dev/null
+++ b/src/clients/xero/provider/exception/XeroProviderException.php
@@ -0,0 +1,20 @@
+getStatusCode(), (string)$response->getBody());
+ }
+}
diff --git a/src/helpers/Provider.php b/src/helpers/Provider.php
index 7c26bd0..96b0f1c 100644
--- a/src/helpers/Provider.php
+++ b/src/helpers/Provider.php
@@ -156,6 +156,7 @@ public static function getPrimaryColor(string $handle): ?string
'wikipedia' => '#000000',
'wordpress' => '#21759B',
'x' => '#0f141a',
+ 'xero' => '#02b7e3',
'xing' => '#046466',
'yahoo' => '#6001D2',
'yammer' => '#106EBE',
@@ -461,6 +462,8 @@ public static function getIcon(string $handle): ?string
'x' => '',
+ 'xero' => '',
+
'xing' => '',
'yahoo' => '',
diff --git a/src/providers/Xero.php b/src/providers/Xero.php
new file mode 100644
index 0000000..784ffb7
--- /dev/null
+++ b/src/providers/Xero.php
@@ -0,0 +1,22 @@
+