Making some notes, as i couldnt find any 100% clear explanations online (there is a more general info on accessing the XERO api via vanilla PHP here tho: https://devblog.xero.com/use-php-to-connect-with-xero-31945bccd037 )
I'm still getting my head around this, will clean up the explanation below, but this might give you a few hints on what to do in the mean time.
Assuming you're already using Laravel, you can install Laravel Socialite ( https://laravel.com/docs/8.x/socialite#installation ) to do the heavy lifting for the oauth2 authentication.
On top of that you'll want to composer require socialiteproviders/xero
(and follow the instructions at https://github.com/socialiteproviders/xero
for hooking it into laravel)
Most likely only the 'admin' user has the permission to 'add' an integration for workflow max, so (i think) you're going to need to authenticate the admin user against your app via oauth2, and then you'll effectively be making api calls as that person - so really think about the implications of this! (at the moment we're only wanting read-only usage of some of the job and quote data so dont need to worry too much).
Create a secret (protected) url endpoint that can be used by an admin user to authenticate access to workflow max, save out their access_token and refresh_token into a database (creating a custom model for that). We'll then use socialite again for google-based access control to the application we're building for general users, so they can only log in with a business google g-cloud login. We'll then use the access_token for our workflow max admin user that we've saved to pull the workflow max job data we need for users to view (and cache it).
Im still in hacking-mode, so the following i've just bashed into Routes/web.php to try stuff out - i'll be completely refactoring into models and controllers in a bit:
// note you need the offline access scope to get a refresh token - access_tokens only last for 30 mins, so for continued access you'll
// need to use the refresh token to ask for a new access token after the 30 mins have expired. I need to automate the refresh,
// at the mo i've just added a route below for manually refreshing the token and storing it in a database-backed model.
// initial url to get the workflowmax admin user to go to:
Route::get('/auth/redirect', function () {
return Socialite::driver('xero')->setScopes(['workflowmax', 'profile', 'email', 'openid', 'offline_access',])->redirect();
});
// hacked together callback code that xero will redirect the admin user back to - a user can have access to multiple xero products,
// so i'm gonna loop over the 'tenants' in the response, and store the workflowmax tenant id separately in the database, as
// we'll need to pass that into api calls.
Route::get('/xero-auth/callback', function () { // xero-auth/callback
$user = Socialite::driver('xero')->setScopes(['workflowmax', 'profile', 'email', 'openid', 'offline_access',])->user();
$wfm_tenant_id = null;
foreach($user->tenants as $tenant) {
if($tenant->tenantType == 'WORKFLOWMAX') {
$wfm_tenant_id = $tenant->tenantId;
}
}
$wfm_settings = App\Models\WfmSettings::firstOrCreate(
['id' => 1],
[
'token' => $user->token,
'token_expiry' => Date('Y-m-d H:i:s', time() + $user->expiresIn - 1),
'refresh_token' => $user->refreshToken,
'id_uuid' => $user->id,
'nickname' => $user->nickname,
'name' => $user->name,
'email' => $user->email,
'tenants' => json_encode($user->tenants),
'wfm_tenant_id' => $wfm_tenant_id
]
);
$wfm_settings->save();
echo 'done. thanks!';
});
// TEMPORARY(!) url to manually invoke a token refresh using the stored refresh token in the database. Need to refactor and have
// the api calls automatically predict/detect an expired token and automatically refresh it.
Route::get('/debug/refresh-token', function() {
$wfm_settings = WfmSettings::find(1);
$http = new GuzzleHttp\Client;
$response = $http->request('POST', 'https://identity.xero.com/connect/token', [
'debug' => true,
'headers' => [
'Authorization' => "Basic ".base64_encode(trim(env('WFM_CLIENT_ID')).":".trim(env('WFM_CLIENT_SECRET'))),
'User-Agent' => 'my-bot-name/1.0',
'Accept' => 'application/json',
],
'form_params' => [
'grant_type' => 'refresh_token',
'refresh_token' => $wfm_settings->refresh_token,
],
]);
$json = json_decode($response->getBody());
$wfm_settings->token = $json->access_token;
$wfm_settings->token_expiry = Date('Y-m-d H:i:s', time() + $json->expires_in - 1);
$wfm_settings->refresh_token = $json->refresh_token;
$wfm_settings->save();
echo 'token refreshed.';
});
// quick test to check we can call the api and get some data back - in this case dump out all the open jobs.
// calls to api endpoints require the access token and the tenant id in the header, if you miss them/get them wrong
// you'll either get an 403 unauthorized error, or a 404 file not found error (eg if you miss off the tenant id header).
Route::get('/test/jobs', function() {
$wfm_settings = WfmSettings::find(1);
$http = new GuzzleHttp\Client;
$response = $http->request('GET', 'https://api.xero.com/workflowmax/3.0/job.api/current?detailed=true', [
// 'debug' => true,
'headers' => [
'Authorization' => "Bearer ".$wfm_settings->token,
'User-Agent' => 'my-bot-name/1.0',
'Accept' => 'application/xml',
'Xero-tenant-id' => $wfm_settings->wfm_tenant_id,
],
]);
// convert the xml response into simple php structures by casting it into json and back.
$xml = simplexml_load_string( $response->getBody());
$json = json_encode($xml);
$array = json_decode($json,TRUE);
dd($array);
});