XeroAPI / xero-node-oauth2-app

NodeJS app for demonstrating the xero-node v4 SDK
MIT License
38 stars 43 forks source link

Click "Try OAuth 2.0" not found on developer.xero.com/myapps and other issues getting started. #48

Closed LiamKarlMitchell closed 4 years ago

LiamKarlMitchell commented 4 years ago

Click "Try OAuth 2.0" is not found.

image

Tried this.

  1. clone project

  2. npm install

  3. Create new .env file (Use a editor when on Windows as it doesn't let you make files starting with .)

  4. Create a new developer account for xero with default company.

  5. Login to developer.xero.com/myapps

  6. Click New app

  7. Enter App name must not already exist Example: Put YourName123456

  8. For OAuth 2.0 grant type select Auth code Web app

  9. Company or application URL: https://localhost Note: Validation won't let you have http.

  10. OAuth 2.0 redirect URI http://localhost:5000/callback

  11. Next

  12. Click Copy on the Client id field.

Paste it into your .env file. PORT=5000 CLIENT_ID=....

  1. Click Generate Secret and Copy it into .env file CLIENT_SECRET=...

  2. Put Redirect URI into .env file REDIRECT_URI=http://localhost:5000/callback

  3. Click save in Xero myapps page and also save .env file

  4. Start the server npm run start-dev

  5. Goto http://localhost:5000

  6. Click Connect to Xero

  7. Choose the Organization (Demo Company for example highly recommended do not use this on a real organization/xero for dev/testing purposes only!) Note: If already connected, just click the Continue button and skip Step 20.

  8. Click Allow access

Tried following this and instructions in readme here. https://devblog.xero.com/xero-oauth-2-api-whats-new-node-example-94715fc8ff19 But those did not work for me only the above did.

Problems:

Note: Clicking Invoices gives me an error. The resource you're looking for cannot be found In the console I see. (Using NZ xero) Account code '500' is not a valid code for this document.

Branding Themes Forbidden

Bank Transfers, Journals, Receipts: Crashes app but is a known issue. https://github.com/XeroAPI/xero-node/issues/412

Items: A validation exception occurred

Linked Transactions: ValidationException Account code '497' is not a valid code for this document.

Overpayments: A validation exception occurred Account code '500' is not a valid code for this document.

Payment Services: AuthorizationUnsuccessful insufficent_scope

Payments, Prepayments: A validation exception occurred Account code '500' is not a valid code for this document.

Thoughts on improvements that could be made: Need to re-auth after closing/re-running the app.

Would be nice if it persisted in a session or saved to Json file on disk the tokenSet as this would give an example of persisting the tokenSet for an authenticated access without needing to re-auth all the time.

Quotes: Just waits for very long time did not bother waiting for it to finish loading.

Reports: The resource you're looking for cannot be found

TaxRates: Validation Error, The report tax type is required.

Organisation page info if that helps any.


Name: Demo Company (NZ)
organisationID: 33ff53f4-41c5-4b9f-b3b8-3cb14dcfb2e2

aPIKey:
name: Demo Company (NZ)
legalName: Demo Company (NZ)
paysTax: true
version: NZ
organisationType: COMPANY
baseCurrency: NZD
countryCode: NZ
isDemoCompany: true
organisationStatus: ACTIVE
timezone: NEWZEALANDSTANDARDTIME
organisationEntityType: COMPANY
shortCode: !HjL-V
edition: BUSINESS```

Cheers for the example project however, time for me to look how this app works and add to a project I'm working on.
SerKnight commented 4 years ago

Hi Liam - thank you so much for documenting your process! It is so appreciated.

This project is in need of a readme re-write to match some of the Xero product changes.

This issue will be very helpful - and I hope it gave some good insights into working with the xero-node project.

You also nailed a few of the features we will be adding in the coming weeks:

Would be nice if it persisted in a session or saved to Json file on disk the tokenSet as this would give an example of persisting the tokenSet for an authenticated access without needing to re-auth all the time.

That being said, were you able to get it running and add the appropriate scopes for the testing you wanted to do?

SerKnight commented 4 years ago

Will come back and close this once we have a chance to integrate all the pieces you touched on.

SerKnight commented 4 years ago

Confirmed an issue in the branding themes. Need to debug.. still working to get these fixes in next week.

LiamKarlMitchell commented 4 years ago

Hi @SerKnight ,

Yes I did get Xero Integration and creating Draft Invoices working in my application. It works Per Admin user with a tokenSet persisted to the DB for each.

Although I have not verified the (refresh token) part works yet or what happens after 60 days of no usage.

Handling errors and validation errors from the LineItems was a bit confusing as It doesn't seem to match the Xero Documentation when using this node.js module.

I made a function I can await on that gives me a xero client when given a Koa Context or a User ID integer. Using Postgresql and Massive JS.

Thanks very much this example project it did help me in the long run as the xero node modules readme is not very informative with its examples...

I'll share back the function I made in-case it helps someone.

const xeroConfig = {
    clientId: process.env.XERO_CLIENT_ID || 'Put your test string here maybe up to you not always good idea to put things in repos e.g. if public code!',
    clientSecret: process.env.XERO_CLIENT_SECRET || 'Put your test string for client secret here maybe, not always good idea though might want to load it from a config or env only!',
    redirectUris: [redirectUri],
    // The scopes you need, see comment below for more scopes that can be used, you must have offline_access, openid and profile to use the oAuth 2.
    scopes: 'openid profile email accounting.contacts accounting.transactions accounting.settings offline_access'.split(' ')
    //openid profile email accounting.settings accounting.reports.read accounting.journals.read accounting.contacts accounting.attachments accounting.transactions assets assets.read projects projects.read offline_access
};
// Save a tokenSet against a user id.
async function saveUserTokenSet(user_id, tokenSet) {
    let db = await massive;
    await db.users.update({id: user_id}, {xero_token_set: tokenSet})
}
// Get a XeroClient for a user context or user id.
// Usage: ctx argument can be a koa context for a user id.
//        ctx argument can also be an integer for the user id.
// This expects the user to be an admin.
async function getXeroClient(ctx = null) {
    // Optional ctx
    let xero = null;

    // Use cached Xero Client for Context.
    if (ctx !== null && typeof ctx.state !== 'undefined' && typeof ctx.state.xero !== 'undefined') {
        // TODO: Refresh token if needed?

        // Return cached xero client.
        xero = ctx.state.xero
    } else {
        xero = new XeroClient(xeroConfig);

        // Prevent a race condition by awaiting on initialize.
        await xero.initialize()
    }

    // A variable to store the tokenSet.
    let tokenSet = null;

    let user_id = null;

    // Cache Xero Client for Context or UserId?
    if (ctx !== null) {
        // Overload if an integer is passed as ctx use it as the user_id.
        if (Number.isInteger(ctx)) {
            user_id = ctx;
            ctx = null
        } else {
            user_id = ctx.user.id
        }

        // Reuse xeroTokenSet user context. (Note: This is not persisted as we do not have session data yet?)
        if (typeof ctx.user !== 'undefined') {
            // If the user contexts xero token is not set
            if (typeof ctx.user.xeroTokenSet === 'undefined' && ctx.user.xeroTokenSet !== null) {
                tokenSet = ctx.user.xeroTokenSet
            }

            // TODO: Decide on caching xero client?
            ctx.state.xero = xero
        }
    }

    // If the tokenSet is still not set look it up based on the user id.
    if (tokenSet === null) {
        let db = await massive;
        let tempUserRecord = await db.users.findOne({id: user_id}, {fields: ['xero_token_set']}); // xero_tenant_id
        if (tempUserRecord === null) {
            // In practice this should never happen.
            throw new Error(`User ID ${user_id} not found.`)
        }
        if (tempUserRecord.xero_token_set !== null) {
            tokenSet = new TokenSet(tempUserRecord.xero_token_set)

            // Cache our tenant id that we are interested in using.
            // if (tempUserRecord.xero_tenant_id !== null) {
            //     xero.tenants = [ { tenantId: tempUserRecord.xero_tenant_id } ]
            // }
        }

        // Store it, but this effectively does nothing as ctx.user is not persisted between requests.
        if (ctx !== null && typeof ctx.user !== 'undefined' && tokenSet) {
            ctx.user.xeroTokenSet = tokenSet
        }
    }

    if (tokenSet !== null) {
        await xero.setTokenSet(tokenSet);

        tokenSet = await xero.readTokenSet();
        if (tokenSet.expired()) {
            // refresh etc.

            // TODO: Attempt to refresh tokenSet.
            tokenSet = await xero.refreshToken();
            await saveUserTokenSet(user_id, tokenSet)
        }

        // Update Tenants.
        await xero.updateTenants();

        if (xero.tenants.length === 0) {
            console.warn(`No tenants found for Xero Connection for ${user_id}`)
            // TODO: Cache tenants information so we only need to get it once after auth?
            // We only really need the tenantId I think.

            // await db.users.update({ id: user_id }, { xero_tenant_id: null })
        } else {
            // await db.users.update({ id: user_id }, { xero_tenant_id: xero.tenants[0].tenantId })
        }
    }

    return xero
}

We have a similar record in the app that must have a Xero Contact ID on it. If possible you can have a function that sets these for existing Records if the name or email matches in Xero Contacts, or perhaps even create the contacts in Xero if they do not exist.

Same with Products, to use the itemCode in the Invoice Line Items.

Usage example in my handler for a post request to export things.

async export(ctx) {
        const xero = await getXeroClient(ctx);
// Handle authentication with the Xero oAuth v4
    async auth(ctx) {
        let response = {
            // Default to requires authentication.
            requiresAuth: true
        };

        const xero = await getXeroClient(ctx);

        // TODO: Check if need to auth as the tokenSet may already be authenticated.

        // let tokenSet = xero.readTokenSet()
        // if (tokenSet.expired()) {
        //     // refresh etc.
        //     await xero.refreshToken()
        // }
        // response.requiresAuth = false

        // `buildConsentUrl()` calls `await xero.initialize()`
        response.redirectUrl = await xero.buildConsentUrl();
        console.log(response.redirectUrl);

        return response
    }

    async callback(ctx) {
        let result = {success: false};
        const xero = await getXeroClient(ctx);

        try {
            // Fudge the URL to what XeroClient expects. (Note: I have a SPA using Quasar and Vue, my routes have a # and so the redirect URI had to be fudged a bit... Happy to share how I did this if someone has same problem basically I couldn't redirect to my API due to a bearer token not being on the request when redirected back from Xero but the front end doesn't allow /xero_callback as it expects ```/#/xero_callback``` I did want it to goto ```/#/xero_sync/callback``` so that my user was back at the same page they clicked the sync button on, but Xero doesn't allow the URL to have a URL Fragment ```#``` symbol! how annoying.

            const tokenSet = await xero.apiCallback(redirectUri + ctx.search);

            // Update user and save the xero_token_set.
            await saveUserTokenSet(ctx.user.id, tokenSet);

            // Get tenants.
            // await xero.updateTenants()
            //
            // result.tenants = xero.tenants
            // TODO: Get info regarding the Xero User.

            result.success = true
        } catch (e) {
            // TODO: Log errors propperly?
            console.info(e);
            result.error = e.message
        }

        // TODO: Update the token set on the user context (Note: This is not persisted between refreshes so this doesn't have any effect actually as the JWT token is only built once on auth.)
        // ctx.user.xeroTokenSet = tokenSet
        ctx.body = result
    }

Get the last moment of end of previous month. Useful to export last months invoices.

            var syncEndDate = new Date(); // Current Date
            syncEndDate.setDate(1); // Goto 1st of the month.
            syncEndDate.setHours(-1, 59, 59, 999); // Go back an hour but end of day.

For each thing exported I store the InvoiceID when successful against the orders in our db. So that they are not processed into draft invoices a second time.

I would like to know more about an example of handling the invalid or warnings when submitting a new invoice.

statusAttributeString does not seem to exist on the response? Xero documentation said it should have a status of OK WARNING or ERROR The behavior instead is that xero.accountingApi.createInvoice can throw an exception. Any validation errors on line items are not actually showing per line item in the error object but in an array. (Pretty hard to know which line item failed or had warnings)

By having a single function I could await on to get a Xero client I was able to get it easily per admin user. In practice, we all use the same Xero Organisation but due to authentication and the need to use oAuth I thought it best that each admin/user authenticated.

It might also be a good idea to have a scheduled task every 30 day or something to refresh auth tokens. But the admin will export every month in our system which is less than the 60 day grace period so should be fine for my use case.

Sorry for the brain dump/large comment.

SerKnight commented 4 years ago

Hi Liam - thanks again for the long list of issues you faced while setting up the repo. I've done a large sweep to both show the functionality of two new api sets we just added: https://github.com/XeroAPI/xero-node/releases/tag/4.6.0

But also to cleanup the documentation of this sample app.. I've also added persistent session storage so that a typescript recompile doesn't wipe out a valid tokenSet.. However please not in order to work with previous session dat between sessions you need to hit the "/" route by clicking the "Home" link in the header.

--

Its a bit hard to grok all your questions, but if they are xero-node specific please open each issue one at a time and we will work through them all! Hope this repo has been helpful with your business needs <3