1

I've been trying to learn how to use the Account API and decided to download and use the XeroOAuth2Sample that's provided on the docs. (https://github.com/XeroAPI/xero-netstandard-oauth2-samples)

So within the HomeController.cs, there is an example HTTPGet method used to retrieve the total number of Invoices from the API which is provided by the Xero docs. The code is as follows:

[HttpGet]
        [Authorize]
        public async Task<IActionResult> OutstandingInvoices()
        {
            var token = await _tokenStore.GetAccessTokenAsync(User.XeroUserId());

            var connections = await _xeroClient.GetConnectionsAsync(token);

            if (!connections.Any())
            {
                return RedirectToAction("NoTenants");
            }

            var data = new Dictionary<string, int>();

            foreach (var connection in connections)
            {
                var accessToken = token.AccessToken;
                var tenantId = connection.TenantId.ToString();

                var organisations = await _accountingApi.GetOrganisationsAsync(accessToken, tenantId);
                var organisationName = organisations._Organisations[0].Name;

                var outstandingInvoices = await _accountingApi.GetInvoicesAsync(accessToken, tenantId, statuses: new List<string>{"AUTHORISED"}, where: "Type == \"ACCREC\"");

                data[organisationName] = outstandingInvoices._Invoices.Count;
            }

            var model = new OutstandingInvoicesViewModel
            {
                Name = $"{User.FindFirstValue(ClaimTypes.GivenName)} {User.FindFirstValue(ClaimTypes.Surname)}",
                Data = data
            };

            return View(model);
        }

So I have been trying to practice and explore the API by making a page that would eventually call the Contacts from the API endpoint. I have created a Contact.cs Model class that looks like so:

public class Contact
    {
        public string ContactID { get; set; }
        public string ContactStatus { get; set; }
        public string Name { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string EmailAddress { get; set; }
        public string SkypeUserName { get; set; }
        public string BankAccountDetails { get; set; }
        public string TaxNumber { get; set; }
        public string AccountsReceivableTaxType { get; set; }
        public string AccountsPayableTaxType { get; set; }
        public List<Address> Addresses { get; set; }
        public List<Phone> Phones { get; set; }
        public DateTime UpdatedDateUTC { get; set; }
        public bool IsSupplier { get; set; }
        public bool IsCustomer { get; set; }
        public string DefaultCurrency { get; set; }
    }

I have then created a ContactViewModel.cs which has the properties that I want to later on display on my Razor View page with the following code:

public class ContactViewModel
    {
        public string ContactID { get; set; }
        public string ContactStatus { get; set; }
        public string Name { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public bool IsSupplier { get; set; }
        public bool IsCustomer { get; set; }
        public string DefaultCurrency { get; set; }
    }

And then, I have my ContactsViewModel which is a list of Contacts from the ContactViewModel:

public class ContactsViewModel
    {
        public List<ContactViewModel> Contacts { get; set; }
    }

So my issue(s) arrise when I try and make / call the HTTPGet request for my Contacts, the code is as follows:

[HttpGet]
        [Authorize]
        public async Task<IActionResult> Contacts()
        {
            var token = await _tokenStore.GetAccessTokenAsync(User.XeroUserId());

            var connections = await _xeroClient.GetConnectionsAsync(token);

            if (!connections.Any())
            {
                return RedirectToAction("NoContacts");
            }   

            foreach (var connection in connections)
            {
                var accessToken = token.AccessToken;
                var tenantId = connection.TenantId.ToString();

                var contactList = await _accountingApi.GetContactsAsync(accessToken, tenantId);

                List<ContactsViewModel> contacts = new List<ContactsViewModel>();

                foreach (var contact in contactList)
                {
                    contacts.Add(new ContactViewModel
                    {
                        ContactID = contact.ContactID,
                        ContactStatus = contact.ContactStatus,
                        Name = contact.Name,
                        FirstName = contact.FirstName,
                        LastName = contact.LastName,
                        IsSupplier = contact.IsSupplier,
                        IsCustomer = contact.IsCustomer

                    });
                }
                contacts.AddRange(contactList);
            }
            var model = new ContactsViewModel()
            {
                //  Contacts = contacts
            };
            return View(model);

        }

So the first error is as follows:

ApiException: Error calling GetContacts: {"title":"Unauthorized","status":401,"detail":"AuthorizationUnsuccessful","instance":"354ff497-d29f-468b-9e1c-4345e9ce8123"}

Which is returned from the "GetContactsAsync" method:

var contactList = await _accountingApi.GetContactsAsync(accessToken, tenantId);

Im unsure if there's specific values that I'm missing which I also need to pass that's causing this error? I couldn't find anything regarding this on the Xero documentation. Although hovering over the GetcontactsAsync displays this for the further info:

(awaitable) Task<Xero.NetStandard.OAuth2.Model.Contacts> IAccountingApiAsync.GetContactsAsync(string accessToken, stringXeroTenantId, [System.DateTime? ifModifiedSince = null], [string where = null], [string order = null], [List<System.Guid> iDs = null], [int? page = null], [bool? includeArchived = null])

And lastly, contactList seems to throw errors as the title suggests regarding the "GetEnumerator" and when the contacts is added to the contactsList using the AddRange, this error is displayed

cannot convert from 'Xero.NetStandard.OAuth2.Model.Contacts' to 'System.Collections.Generic.IEnumerable<XeroOAuth2Sample.Models.ContactsViewModel>'

Is there something that I'm clearly missing out from the GetContactsAsync that should be there? Thank you in advance for reading and helping.

zadders
  • 428
  • 1
  • 8
  • 30

2 Answers2

3

The contacts endpoint requires an additional OAuth2.0 scope than the default scopes asked for in the sample. You can see the set of scopes the sample uses here: https://github.com/XeroAPI/xero-netstandard-oauth2-samples/blob/master/XeroOAuth2Sample/XeroOAuth2Sample/Startup.cs#L105L106

You can see the full set of scopes that can be requested in our documentation here: https://developer.xero.com/documentation/oauth2/scopes

For your case, you'll need to use either of the accounting.contacts or accounting.contacts.read scopes for reading contacts or the accounting.contacts scope if ultimately you want to update/create contacts

Edit: If you only want to read contacts you need to request and have the user grant consent to the accounting.contacts.read scope. If you want to update/create contacts, you need to request and have the user grant consent to the accounting.contacts scope. The accounting.contacts scope will allow read and write access

MJMortimer
  • 865
  • 5
  • 10
  • I'm still a bit confused as to how that would differ from the invoices example provided. For now I'm just trying to GET all contact, not neccessarily update / create. But if I'm not mistaken, I am accessing the accounting scope and then accessing the GetContactsAsync. – zadders Feb 29 '20 at 19:25
  • It's all down to consent. When go through the authorise UI, the app is asking for those 2 default scope, and we're putting a page up effectively saying "this app wants to access this subset of your Xero data" the subset that is allowed to be accessed is determined by scopes. By default your app is not requesting access to the users contacts data, and so your app is not allowed to make requests against the contact endpoint. You need to add one of the contacts scopes and reauthorise your organisation to allow your app to access your contact data – MJMortimer Feb 29 '20 at 20:32
  • Could you show me an example of this being done just so I can know how to relate it to what I'm writing. Because I'm already signing in, and I've called the access token too. – zadders Feb 29 '20 at 22:57
  • Also I've added the further scope potions such as accounting.contacts and contacts.read. Trying to figure out what to do after. – zadders Feb 29 '20 at 23:19
  • It's not very intuitive in the sample because it's supposed to be super barebones, but what you'd do is add the extra scopes in the startup class for the signup-oidc auth scheme, then run the sample, go through the signup flow again to reconsent the new scopes. You don't need to pick a new organisation, you just need to go through the user consent flow again to give consent to the app to use the new scopes on your users behalf – MJMortimer Feb 29 '20 at 23:27
  • in my foreach(var contact in contactList) I get an error saying "foreach statement cannot operator on variables of type 'Contacts' because 'Contacts' does not contain a public instace definition for 'GetEnumerator' – zadders Feb 29 '20 at 23:41
  • The invoices sample works like this: outstandingInvoices._Invoices. I'd say the same applies to your contacts sample. You probably need something like contacts._Contacts to access the actual list of contacts retrieved – MJMortimer Feb 29 '20 at 23:49
1

So after adding further options to the startup.cs :

options.Scope.Add("accounting.contacts");
options.Scope.Add("accounting.contacts.read");

I went through the xero signup process again as suggested to reconsent the new scope. I Think corrected the syntax and some slight logic to my Contacts() HTTPGet method in the HomeController.

 [HttpGet]
        [Authorize]
        public async Task<IActionResult> Contacts()
        {
            var token = await _tokenStore.GetAccessTokenAsync(User.XeroUserId());

            var connections = await _xeroClient.GetConnectionsAsync(token);

            ContactsViewModel contacts = new ContactsViewModel();
            contacts.Contacts = new List<ContactViewModel>();

            if (!connections.Any())
            {
                return RedirectToAction("NoContacts");
            }   

            foreach (var connection in connections)
            {
                var accessToken = token.AccessToken;
                var tenantId = connection.TenantId.ToString();

                Contacts contactList = await _accountingApi.GetContactsAsync(accessToken, tenantId);
                contacts.Contacts.AddRange(contactList._Contacts.Select(contact => new ContactViewModel()
                {
                    ContactID = contact.ContactID.ToString(),
                    ContactStatus = contact.ContactStatus.ToString(),
                    Name = contact.Name,
                    FirstName = contact.FirstName,
                    LastName = contact.LastName,
                    IsSupplier = contact.IsSupplier.Value,
                    IsCustomer = contact.IsCustomer.Value
                }).ToList());
            }

            return View(contacts);
        }
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
zadders
  • 428
  • 1
  • 8
  • 30