1

I use OAuth library from Google in order to set connection with Spotify. There is a problem. When createService() and authCallback() is part of auth object, raised error:

Couldn't find script function: authCallback()

Why is the callback function not visible inside the auth object?

The code for this case:

function doGet() {
    if (auth.hasAccess()) {
        main();
    } else {
        return auth.createFlow();
    }
}

const auth = (function () {
    const CLIENT_ID = '...';
    const CLIENT_SECRET = '...';

    const _service = createService();

    function createService() {
        return OAuth2.createService('spotify')
            .setAuthorizationBaseUrl('https://accounts.spotify.com/authorize')
            .setTokenUrl('https://accounts.spotify.com/api/token')
            .setClientId(CLIENT_ID)
            .setClientSecret(CLIENT_SECRET)
            .setCallbackFunction('authCallback') // set callback
            .setPropertyStore(PropertiesService.getUserProperties())
            .setScope('playlist-read-private playlist-modify-private playlist-modify-public user-library-read')
            .setParam('response_type', 'code')
            .setParam('redirect_uri', getRedirectUri());
    }

    function authCallback(request) {
        let isAuthorized = _service.handleCallback(request);
        if (isAuthorized) {
            return HtmlService.createHtmlOutput('Success! You can close this tab.');
        } else {
            return HtmlService.createHtmlOutput('Denied. You can close this tab');
        }
    }

    ...

    return {
        hasAccess: hasAccess,
        getAccessToken: getAccessToken,
        createFlow: createFlow,
    };
})();

But if do this without auth object, no error and success callback:

function createService() {
    return OAuth2.createService('spotify')
        .setCallbackFunction('authCallback')
        // ...
}

function authCallback(request) {
    // ...
}

I can do this, but then it makes no sense to hide implementation details in the auth object:

const auth = (function () {
    function createService() {
        return OAuth2.createService('spotify')
            .setCallbackFunction('authCallback')
            // ...
    }

    function authCallback(request) {
        // ...
    }

    return {
        // ...
        authCallback: authCallback,
    };
})();

function authCallback(request) {
    return auth.authCallback(request);
}

function doGet() {
    if (auth.hasAccess()) {
        main();
    } else {
        return auth.createFlow();
    }
}

Full code with error

function doGet() {
    if (auth.hasAccess()) {
        main();
    } else {
        return auth.createFlow();
    }
}

const auth = (function () {
    const CLIENT_ID = '...';
    const CLIENT_SECRET = '...';

    const _service = createService();

    function createService() {
        return OAuth2.createService('spotify')
            .setAuthorizationBaseUrl('https://accounts.spotify.com/authorize')
            .setTokenUrl('https://accounts.spotify.com/api/token')
            .setClientId(CLIENT_ID)
            .setClientSecret(CLIENT_SECRET)
            .setCallbackFunction('authCallback')
            .setPropertyStore(PropertiesService.getUserProperties())
            .setScope('playlist-read-private playlist-modify-private playlist-modify-public user-library-read')
            .setParam('response_type', 'code')
            .setParam('redirect_uri', getRedirectUri());
    }

    function authCallback(request) {
        let isAuthorized = _service.handleCallback(request);
        if (isAuthorized) {
            return HtmlService.createHtmlOutput('Success! You can close this tab.');
        } else {
            return HtmlService.createHtmlOutput('Denied. You can close this tab');
        }
    }

    function getRedirectUri() {
        let scriptId = encodeURIComponent(ScriptApp.getScriptId());
        let template = 'https://script.google.com/macros/d/%s/usercallback';
        return Utilities.formatString(template, scriptId);
    }

    function hasAccess() {
        return _service.hasAccess();
    }

    function getAccessToken() {
        return _service.getAccessToken();
    }

    function createFlow() {
        let template = '<a href="%s" target="_blank">Authorize</a>';
        let html = Utilities.formatString(template, _service.getAuthorizationUrl());
        return HtmlService.createHtmlOutput(html);
    }

    return {
        hasAccess: hasAccess,
        getAccessToken: getAccessToken,
        createFlow: createFlow,
    };
})();

6
  • 2
    If you don't expose authCallback in your IIFE, how would the caller be able to invoke it? Commented Sep 6, 2020 at 15:48
  • Who exactly are you trying to hide it from? Commented Sep 6, 2020 at 16:29
  • What are the benefits that you are looking for to declare authCallback inside an object instead of doing this at the global scope? Commented Sep 6, 2020 at 17:04
  • @Rubén encapsulation Commented Sep 6, 2020 at 17:25
  • You can't have your cake and eat it :D Please read Object-oriented JavaScript for beginners Commented Sep 6, 2020 at 17:33

1 Answer 1

2

The value you pass into setCallbackFunction() actually gets passed into the StateTokenBuilder.withMethod() method, which does not require the argument to be available in the global scope. But that means you need to pass it the string 'auth.authCallback'. Simply passing it 'authCallback' won't work because there is no function in the global scope with that name.

So then it also means that you need to expose authCallback in your return statement so that it becomes available in the global scope as auth.authCallback.

const auth = (function () {
    const CLIENT_ID = '...';
    const CLIENT_SECRET = '...';

    const _service = createService();

    function createService() {
        return OAuth2.createService('spotify')
            .setAuthorizationBaseUrl('https://accounts.spotify.com/authorize')
            .setTokenUrl('https://accounts.spotify.com/api/token')
            .setClientId(CLIENT_ID)
            .setClientSecret(CLIENT_SECRET)
            .setCallbackFunction('auth.authCallback') // Use correct method name
            .setPropertyStore(PropertiesService.getUserProperties())
            .setScope('playlist-read-private playlist-modify-private playlist-modify-public user-library-read')
            .setParam('response_type', 'code')
            .setParam('redirect_uri', getRedirectUri());
    }

    function authCallback(request) {
        let isAuthorized = _service.handleCallback(request);
        if (isAuthorized) {
            return HtmlService.createHtmlOutput('Success! You can close this tab.');
        } else {
            return HtmlService.createHtmlOutput('Denied. You can close this tab');
        }
    }

    function getRedirectUri() {
        let scriptId = encodeURIComponent(ScriptApp.getScriptId());
        let template = 'https://script.google.com/macros/d/%s/usercallback';
        return Utilities.formatString(template, scriptId);
    }

    function hasAccess() {
        return _service.hasAccess();
    }

    function getAccessToken() {
        return _service.getAccessToken();
    }

    function createFlow() {
        let template = '<a href="%s" target="_blank">Authorize</a>';
        let html = Utilities.formatString(template, _service.getAuthorizationUrl());
        return HtmlService.createHtmlOutput(html);
    }

    return {
        hasAccess: hasAccess,
        getAccessToken: getAccessToken,
        createFlow: createFlow,
        authCallback: authCallback // Expose the method
    };
})();

Just to help clarify the purpose of authCallback(), try renaming it to something like displayAuthSuccessOrFailure(). All it's doing is presenting a success or failure message to the end user. This may alter how you think about its exposure/encapsulation.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.