Skip to content

Commit 9b7c968

Browse files
committed
feat: add job to renew token automatically, add ability to unlink
1 parent 8b283e6 commit 9b7c968

File tree

8 files changed

+279
-65
lines changed

8 files changed

+279
-65
lines changed

LICENSE

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
BSD 3-Clause License
2+
3+
Copyright (c) Fullscreen Interactive All rights reserved.
4+
5+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6+
7+
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8+
9+
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
10+
11+
Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
12+
13+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Silverstripe support for authenication and connecting applications via oauth.
1616
To setup register a Xero Application and define your clientId and clientSecret
1717
as environment variables.
1818

19-
```
19+
```yaml
2020
XERO_CLIENT_ID='123'
2121
XERO_CLIENT_SECRET='123'
2222
```
@@ -25,13 +25,105 @@ Once those API keys are available, a new tab under the `Settings` admin will
2525
appear for connecting to Xero. Follow the prompts to link the selected account
2626
to your Silverstripe website.
2727

28+
## Renewing the Access Token
29+
30+
Access tokens last 30 days, a scheduled queued job (`RefreshXeroTokenJob`) is
31+
provided which renews the access token on a regular basis.
32+
33+
## Setting up the application in Xero
34+
35+
1. Head to https://developer.xero.com/app/manage/
36+
1. Create a new `Web App`
37+
1. Set the `Redirect URI` to be `https://www.yoursite.com/connectXero`
38+
39+
Note the `connectXero` endpoint in Silverstripe is restricted to `ADMIN` only
40+
users.
41+
2842
## Interacting with the API
2943

30-
```
44+
```php
3145
/** @var \XeroPHP\Application **/
3246
$app = XeroFactory::singleton()->getApplication();
3347
```
3448

3549
Integrating with the API is done via https://github.com/calcinai/xero-php.
3650
Consult that page for further information for creating invoices etc.
3751

52+
#### Creating a Xero Contact from a Member
53+
54+
```php
55+
<?php
56+
57+
use Psr\Log\LoggerInterface;
58+
use SilverStripe\ORM\DataExtension;
59+
use FullscreenInteractive\SilverStripeXero\XeroFactory;
60+
use SilverStripe\Core\Injector\Injector;
61+
62+
class XeroMemberExtension extends DataExtension
63+
{
64+
private static $db = [
65+
'XeroContactID' => 'Varchar'
66+
];
67+
68+
public function updateCMSFields(FieldList $fields)
69+
{
70+
$fields->makeFieldReadonly('XeroContactID');
71+
}
72+
73+
public function getXeroContact()
74+
{
75+
try {
76+
$xero = XeroFactory::singleton()->getApplication();
77+
} catch (Throwable $e) {
78+
$xero = null;
79+
}
80+
81+
if ($xero) {
82+
try {
83+
$contact = null;
84+
85+
if ($id = $this->owner->XeroContactID) {
86+
$contact = $xero->loadByGUID('Accounting\\Contact', $id);
87+
}
88+
89+
if (!$contact) {
90+
$existing = $xero->load('Accounting\\Contact')
91+
->where('EmailAddress!=null AND EmailAddress.Contains("' . trim($this->owner->Email) . '")')
92+
->execute();
93+
94+
if (count($existing) > 1) {
95+
$contact = $existing->offsetGet(0);
96+
}
97+
}
98+
99+
if (!$contact) {
100+
// create the record
101+
$contact = new \XeroPHP\Models\Accounting\Contact();
102+
$contact->setName($this->owner->Name);
103+
$contact->setFirstName($this->owner->FirstName);
104+
$contact->setLastName($this->owner->Surname);
105+
$contact->setEmailAddress($this->owner->Email);
106+
107+
try {
108+
$xero->save($contact);
109+
} catch (Exception $e) {
110+
if (strpos($e->getMessage(), 'Already assigned to') !== false) {
111+
$contact->setName($this->owner->Name . ' ' . date('Y-m-d'));
112+
113+
try {
114+
$xero->save($contact);
115+
} catch (Exception $e) {
116+
Injector::inst()->get(LoggerInterface::class)->warning($e);
117+
}
118+
}
119+
}
120+
}
121+
122+
return $contact;
123+
} catch (Exception $e) {
124+
Injector::inst()->get(LoggerInterface::class)->error($e);
125+
}
126+
}
127+
}
128+
}
129+
```

_config/jobs.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
SilverStripe\Core\Injector\Injector:
2+
Symbiote\QueuedJobs\Services\QueuedJobService:
3+
properties:
4+
defaultJobs:
5+
RefreshXeroToken:
6+
type: 'FullscreenInteractive\SilverStripeXero\RefreshXeroTokenJob'
7+
filter:
8+
JobTitle: 'Refresh Xero token'
9+
construct:
10+
title: 'Refresh Xero token'
11+
startDateFormat: 'Y-m-d H:i:s'
12+
startTimeString: 'next week 00:01'
13+
recreate: 1

composer.json

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,9 @@
1616
],
1717
"require": {
1818
"silverstripe/framework": "^4",
19+
"symbiote/silverstripe-queuedjobs": "^4 || ^5",
1920
"calcinai/xero-php": "^2.3"
2021
},
21-
"require-dev": {
22-
"phpunit/phpunit": "^5.7",
23-
"squizlabs/php_codesniffer": "^3"
24-
},
25-
"replace": {
26-
"silverstripe/googlesitemaps": "*"
27-
},
2822
"extra": {
2923
"branch-alias": {
3024
"dev-master": "1.x-dev"

src/RefreshXeroTokenJob.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace FullscreenInteractive\SilverStripeXero;
4+
5+
use Symbiote\QueuedJobs\Services\AbstractQueuedJob;
6+
7+
class RefreshXeroTokenJob extends AbstractQueuedJob
8+
{
9+
public function getTitle()
10+
{
11+
return 'Refresh Xero token';
12+
}
13+
14+
15+
public function process()
16+
{
17+
$xero = XeroFactory::create();
18+
$date = $xero->renewToken();
19+
20+
$this->addMessage('Token refreshed until: '. date('d/m/Y H:i:s', $date));
21+
22+
$this->isComplete = true;
23+
}
24+
}

src/XeroController.php

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33
namespace FullscreenInteractive\SilverStripeXero;
44

55
use SilverStripe\Control\Controller;
6-
use SilverStripe\Control\Director;
76
use SilverStripe\Security\Permission;
7+
use SilverStripe\Security\SecurityToken;
88
use SilverStripe\SiteConfig\SiteConfig;
99

1010
class XeroController extends Controller
1111
{
1212
private static $scope = 'openid offline_access email profile accounting.contacts accounting.contacts.read accounting.transactions accounting.transactions.read';
1313

14+
private static $allowed_actions = [
15+
'index',
16+
'unlink'
17+
];
18+
1419
public function index()
1520
{
16-
$url = self::join_links(Director::absoluteBaseURL() . 'xero');
17-
1821
$provider = XeroFactory::singleton()->getProvider();
1922

2023
if (!isset($_GET['code'])) {
@@ -41,26 +44,65 @@ public function index()
4144
return $this->httpError(401);
4245
}
4346

44-
$obj = SiteConfig::current_site_config();
45-
$obj->XeroAccessToken = $token->getToken();
47+
$config = SiteConfig::current_site_config();
48+
$config->XeroAccessToken = $token->getToken();
4649

4750
$refresh = $token->getRefreshToken();
4851

4952
if ($refresh) {
50-
$obj->XeroRefreshToken = $refresh;
53+
$config->XeroRefreshToken = $refresh;
54+
$config->XeroTokenRefreshExpires = $token->getExpires();
55+
} else {
56+
$config->XeroRefreshToken = null;
57+
$config->XeroTokenRefreshExpires = null;
5158
}
5259

53-
$obj->write();
5460
$tenants = $provider->getTenants($token);
5561

56-
foreach ($tenants as $tenant) {
57-
$id = $tenant->tenantId;
62+
$config->XeroTenants = serialize($tenants);
63+
$config->write();
64+
65+
if ($tenants) {
66+
$id = null;
67+
68+
foreach ($tenants as $tenant) {
69+
$id = $tenant->tenantId;
5870

59-
$obj->XeroTenantId = $id;
60-
$obj->write();
71+
if (!$config->XeroTenantId) {
72+
$config->XeroTenantId = $id;
73+
$config->write();
6174

62-
return $this->redirect('admin/settings/?doneGlobal=1');
75+
return $this->redirect('admin/settings/?connectedXeroTo='. $id. '#Root_Xero');
76+
}
77+
}
78+
79+
if ($id) {
80+
$config->XeroTenantId = $id;
81+
$config->write();
82+
83+
return $this->redirect('admin/settings/?connectedXeroTo='. $id . '#Root_Xero');
84+
}
6385
}
86+
87+
return $this->redirect('admin/settings/#Root_Xero');
6488
}
6589
}
90+
91+
92+
public function unlink()
93+
{
94+
if (!SecurityToken::inst()->checkRequest($this->request)) {
95+
return $this->httpError(400);
96+
}
97+
98+
$config = SiteConfig::current_site_config();
99+
$config->XeroTenantId = null;
100+
$config->XeroTenants = null;
101+
$config->XeroTokenRefreshExpires = null;
102+
$config->XeroRefreshToken = null;
103+
$config->XeroAccessToken = null;
104+
$config->write();
105+
106+
return $this->redirect('admin/settings/#Root_Xero');
107+
}
66108
}

0 commit comments

Comments
 (0)