function decodeJWTPayload(token) {
    const [, payloadStr] = token.split('.');
    return JSON.parse(atob(payloadStr));
}
function isExpiredJWT(token) {
    const { exp } = decodeJWTPayload(token);
    return isExpiredInSeconds(exp || 0, 60); // exp or always, and 1 minute earlier
}
function isExpiredInSeconds(expiredTime, costMore = 0) {
    const time = Math.ceil(new Date().getTime() / 1000) + costMore;
    return time >= expiredTime;
}
// create popup
function createPopup(config) {
    const url = config.url || '/';
    const name = config.name || 'SheetbaseOAuthLogin'; // no space for IE
    const options = config.options || ('location=0,status=0' +
        ',width=' + window.innerWidth +
        ',height=' + window.innerHeight);
    const callback = config.callback || (() => true);
    // launch window
    const oauthWindow = window.open(url, name, options);
    // cackback
    const oauthInterval = window.setInterval(() => {
        if (oauthWindow.closed) {
            window.clearInterval(oauthInterval);
            callback();
        }
    }, 1000);
}
// get app host
function getHost() {
    let host;
    // get from base tag
    // else from window.location.href
    const baseHref = ((document.getElementsByTagName('base')[0] || {})['href'] || '').slice(0, -1);
    if (!!baseHref) {
        host = baseHref;
    }
    else {
        const hrefSplit = window.location.href.split('/').filter(Boolean);
        host = hrefSplit[0] + '//' + hrefSplit[1];
    }
    return host;
}

var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
class User {
    constructor(Api, info, idToken, refreshToken) {
        this.Api = Api;
        this.idToken = idToken;
        this.refreshToken = refreshToken;
        this.setInfo(info);
    }
    setInfo(info) {
        const { uid, providerId, email, emailVerified, createdAt, lastLogin, username, phoneNumber, displayName, photoURL, bio, url, address, additionalData, claims, settings, isAnonymous, isNewUser, } = info;
        this.uid = uid;
        this.providerId = providerId;
        this.email = email;
        this.emailVerified = emailVerified;
        this.createdAt = createdAt;
        this.lastLogin = lastLogin;
        this.username = username;
        this.displayName = displayName;
        this.phoneNumber = phoneNumber;
        this.photoURL = photoURL;
        this.bio = bio;
        this.url = url;
        this.address = address;
        this.additionalData = additionalData;
        this.claims = claims;
        this.settings = settings;
        this.isAnonymous = isAnonymous;
        this.isNewUser = isNewUser;
        return info;
    }
    toJSON() {
        const uid = this.uid;
        const providerId = this.providerId;
        const email = this.email;
        const emailVerified = this.emailVerified;
        const createdAt = this.createdAt;
        const lastLogin = this.lastLogin;
        const username = this.username;
        const phoneNumber = this.phoneNumber;
        const displayName = this.displayName;
        const photoURL = this.photoURL;
        const bio = this.bio;
        const url = this.url;
        const address = this.address;
        const additionalData = this.additionalData;
        const claims = this.claims;
        const settings = this.settings;
        const isAnonymous = this.isAnonymous;
        const isNewUser = this.isNewUser;
        return {
            uid,
            providerId,
            email,
            emailVerified,
            createdAt,
            lastLogin,
            username,
            phoneNumber,
            displayName,
            photoURL,
            bio,
            url,
            address,
            additionalData,
            claims,
            settings,
            isAnonymous,
            isNewUser,
        };
    }
    getIdToken(forceRefresh = false) {
        return __awaiter(this, void 0, void 0, function* () {
            // renew
            if (isExpiredJWT(this.idToken) || forceRefresh) {
                const { idToken } = yield this.Api.get('/token', {
                    refreshToken: this.refreshToken,
                });
                this.idToken = idToken;
            }
            return this.idToken;
        });
    }
    getIdTokenResult(forceRefresh = false) {
        return __awaiter(this, void 0, void 0, function* () {
            const idToken = yield this.getIdToken(forceRefresh);
            return decodeJWTPayload(idToken);
        });
    }
    sendEmailVerification() {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.emailVerified) {
                return yield this.Api.put('/oob', {}, {
                    mode: 'verifyEmail',
                    email: this.email,
                });
            }
        });
    }
    updateProfile(profile) {
        return __awaiter(this, void 0, void 0, function* () {
            const newInfo = yield this.Api.post('/user', {}, {
                profile,
            });
            return this.setInfo(newInfo);
        });
    }
    setAdditionalData(data) {
        return __awaiter(this, void 0, void 0, function* () {
            const newInfo = yield this.Api.post('/user/additional', {}, {
                additionalData: data,
            });
            return this.setInfo(newInfo);
        });
    }
    setSettings(data) {
        return __awaiter(this, void 0, void 0, function* () {
            const newInfo = yield this.Api.post('/user/settings', {}, {
                settings: data,
            });
            return this.setInfo(newInfo);
        });
    }
    setProfilePublicly(props) {
        return __awaiter(this, void 0, void 0, function* () {
            const newInfo = yield this.Api.post('/user/publicly', {}, {
                publicly: props,
            });
            return this.setInfo(newInfo);
        });
    }
    setProfilePrivately(props) {
        return __awaiter(this, void 0, void 0, function* () {
            const newInfo = yield this.Api.post('/user/privately', {}, {
                privately: props,
            });
            return this.setInfo(newInfo);
        });
    }
    setUsername(username) {
        return __awaiter(this, void 0, void 0, function* () {
            const newInfo = yield this.Api.post('/user/username', {}, {
                username,
            });
            return this.setInfo(newInfo);
        });
    }
    changePassword(currentPassword, newPassword) {
        return __awaiter(this, void 0, void 0, function* () {
            return yield this.Api.post('/user/password', {}, {
                currentPassword,
                newPassword,
            });
        });
    }
    logout() {
        return __awaiter(this, void 0, void 0, function* () {
            return yield this.Api.delete('/');
        });
    }
    delete() {
        return __awaiter(this, void 0, void 0, function* () {
            return yield this.Api.delete('/cancel', {}, {
                refreshToken: this.refreshToken,
            });
        });
    }
}

class AuthProvider {
    constructor(providerId, endpoint, scopes) {
        this.providerId = providerId;
        this.endpoint = endpoint;
        this.scopes = scopes;
    }
    addScope(scope) {
        this.scopes = this.scopes + ' ' + scope;
    }
    setCustomParameters(customOAuthParameters) {
        this.customParameters = customOAuthParameters;
    }
    url(clientId, redirectUri) {
        let params = '';
        if (!!this.customParameters) {
            for (const key of Object.keys(this.customParameters)) {
                params += '&' + key + '=' + this.customParameters[key];
            }
        }
        return this.endpoint + '?' +
            `response_type=token&` +
            `client_id=${clientId}&` +
            `redirect_uri=${redirectUri}&` +
            `scope=${this.scopes}` +
            params;
    }
}

var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};

function createCommonjsModule(fn, module) {
	return module = { exports: {} }, fn(module, module.exports), module.exports;
}

var pubsub = createCommonjsModule(function (module, exports) {
/**
 * Copyright (c) 2010,2011,2012,2013,2014 Morgan Roderick http://roderick.dk
 * License: MIT - http://mrgnrdrck.mit-license.org
 *
 * https://github.com/mroderick/PubSubJS
 */

(function (root, factory){

    var PubSub = {};
    root.PubSub = PubSub;

    var define = root.define;

    factory(PubSub);

    // AMD support
    if (typeof define === 'function' && define.amd){
        define(function() { return PubSub; });

        // CommonJS and Node.js module support
    } else {
        if (module !== undefined && module.exports) {
            exports = module.exports = PubSub; // Node.js specific `module.exports`
        }
        exports.PubSub = PubSub; // CommonJS module 1.1.1 spec
        module.exports = exports = PubSub; // CommonJS
    }

}(( typeof window === 'object' && window ) || commonjsGlobal, function (PubSub){

    var messages = {},
        lastUid = -1;

    function hasKeys(obj){
        var key;

        for (key in obj){
            if ( obj.hasOwnProperty(key) ){
                return true;
            }
        }
        return false;
    }

    /**
     * Returns a function that throws the passed exception, for use as argument for setTimeout
     * @alias throwException
     * @function
     * @param { Object } ex An Error object
     */
    function throwException( ex ){
        return function reThrowException(){
            throw ex;
        };
    }

    function callSubscriberWithDelayedExceptions( subscriber, message, data ){
        try {
            subscriber( message, data );
        } catch( ex ){
            setTimeout( throwException( ex ), 0);
        }
    }

    function callSubscriberWithImmediateExceptions( subscriber, message, data ){
        subscriber( message, data );
    }

    function deliverMessage( originalMessage, matchedMessage, data, immediateExceptions ){
        var subscribers = messages[matchedMessage],
            callSubscriber = immediateExceptions ? callSubscriberWithImmediateExceptions : callSubscriberWithDelayedExceptions,
            s;

        if ( !messages.hasOwnProperty( matchedMessage ) ) {
            return;
        }

        for (s in subscribers){
            if ( subscribers.hasOwnProperty(s)){
                callSubscriber( subscribers[s], originalMessage, data );
            }
        }
    }

    function createDeliveryFunction( message, data, immediateExceptions ){
        return function deliverNamespaced(){
            var topic = String( message ),
                position = topic.lastIndexOf( '.' );

            // deliver the message as it is now
            deliverMessage(message, message, data, immediateExceptions);

            // trim the hierarchy and deliver message to each level
            while( position !== -1 ){
                topic = topic.substr( 0, position );
                position = topic.lastIndexOf('.');
                deliverMessage( message, topic, data, immediateExceptions );
            }
        };
    }

    function messageHasSubscribers( message ){
        var topic = String( message ),
            found = Boolean(messages.hasOwnProperty( topic ) && hasKeys(messages[topic])),
            position = topic.lastIndexOf( '.' );

        while ( !found && position !== -1 ){
            topic = topic.substr( 0, position );
            position = topic.lastIndexOf( '.' );
            found = Boolean(messages.hasOwnProperty( topic ) && hasKeys(messages[topic]));
        }

        return found;
    }

    function publish( message, data, sync, immediateExceptions ){
        message = (typeof message === 'symbol') ? message.toString() : message;

        var deliver = createDeliveryFunction( message, data, immediateExceptions ),
            hasSubscribers = messageHasSubscribers( message );

        if ( !hasSubscribers ){
            return false;
        }

        if ( sync === true ){
            deliver();
        } else {
            setTimeout( deliver, 0 );
        }
        return true;
    }

    /**
     * Publishes the message, passing the data to it's subscribers
     * @function
     * @alias publish
     * @param { String } message The message to publish
     * @param {} data The data to pass to subscribers
     * @return { Boolean }
     */
    PubSub.publish = function( message, data ){
        return publish( message, data, false, PubSub.immediateExceptions );
    };

    /**
     * Publishes the the message synchronously, passing the data to it's subscribers
     * @function
     * @alias publishSync
     * @param { String } message The message to publish
     * @param {} data The data to pass to subscribers
     * @return { Boolean }
     */
    PubSub.publishSync = function( message, data ){
        return publish( message, data, true, PubSub.immediateExceptions );
    };

    /**
     * Subscribes the passed function to the passed message. Every returned token is unique and should be stored if you need to unsubscribe
     * @function
     * @alias subscribe
     * @param { String } message The message to subscribe to
     * @param { Function } func The function to call when a new message is published
     * @return { String }
     */
    PubSub.subscribe = function( message, func ){
        if ( typeof func !== 'function'){
            return false;
        }

        message = (typeof message === 'symbol') ? message.toString() : message;

        // message is not registered yet
        if ( !messages.hasOwnProperty( message ) ){
            messages[message] = {};
        }

        // forcing token as String, to allow for future expansions without breaking usage
        // and allow for easy use as key names for the 'messages' object
        var token = 'uid_' + String(++lastUid);
        messages[message][token] = func;
        
        // return token for unsubscribing
        return token;
    };

    /**
     * Subscribes the passed function to the passed message once
     * @function
     * @alias subscribeOnce
     * @param { String } message The message to subscribe to
     * @param { Function } func The function to call when a new message is published
     * @return { PubSub }
     */
    PubSub.subscribeOnce = function( message, func ){
        var token = PubSub.subscribe( message, function(){
            // before func apply, unsubscribe message
            PubSub.unsubscribe( token );
            func.apply( this, arguments );
        });
        return PubSub;
    };

    /**
     * Clears all subscriptions
     * @function
     * @public
     * @alias clearAllSubscriptions
     */
    PubSub.clearAllSubscriptions = function clearAllSubscriptions(){
        messages = {};
    };

    /**
     * Clear subscriptions by the topic
     * @function
     * @public
     * @alias clearAllSubscriptions
     */
    PubSub.clearSubscriptions = function clearSubscriptions(topic){
        var m;
        for (m in messages){
            if (messages.hasOwnProperty(m) && m.indexOf(topic) === 0){
                delete messages[m];
            }
        }
    };

    /**
     * Removes subscriptions
     *
     * - When passed a token, removes a specific subscription.
     *
	 * - When passed a function, removes all subscriptions for that function
     *
	 * - When passed a topic, removes all subscriptions for that topic (hierarchy)
     * @function
     * @public
     * @alias subscribeOnce
     * @param { String | Function } value A token, function or topic to unsubscribe from
     * @example // Unsubscribing with a token
     * var token = PubSub.subscribe('mytopic', myFunc);
     * PubSub.unsubscribe(token);
     * @example // Unsubscribing with a function
     * PubSub.unsubscribe(myFunc);
     * @example // Unsubscribing from a topic
     * PubSub.unsubscribe('mytopic');
     */
    PubSub.unsubscribe = function(value){
        var descendantTopicExists = function(topic) {
                var m;
                for ( m in messages ){
                    if ( messages.hasOwnProperty(m) && m.indexOf(topic) === 0 ){
                        // a descendant of the topic exists:
                        return true;
                    }
                }

                return false;
            },
            isTopic    = typeof value === 'string' && ( messages.hasOwnProperty(value) || descendantTopicExists(value) ),
            isToken    = !isTopic && typeof value === 'string',
            isFunction = typeof value === 'function',
            result = false,
            m, message, t;

        if (isTopic){
            PubSub.clearSubscriptions(value);
            return;
        }

        for ( m in messages ){
            if ( messages.hasOwnProperty( m ) ){
                message = messages[m];

                if ( isToken && message[value] ){
                    delete message[value];
                    result = value;
                    // tokens are unique, so we can just stop here
                    break;
                }

                if (isFunction) {
                    for ( t in message ){
                        if (message.hasOwnProperty(t) && message[t] === value){
                            delete message[t];
                            result = true;
                        }
                    }
                }
            }
        }

        return result;
    };
}));
});
var pubsub_1 = pubsub.publish;
var pubsub_2 = pubsub.subscribe;
var pubsub_3 = pubsub.PubSub;

var __awaiter$1 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
class AuthService {
    constructor(app) {
        this.SHEETBASE_USER_CHANGED = 'SHEETBASE_USER_CHANGED';
        this.SHEETBASE_USER_INFO = 'user_info';
        this.SHEETBASE_USER_CREDS = 'user_creds';
        this.currentUser = null;
        this.app = app;
        this.Api = this.app.Api
            .addBeforeHooks((data) => __awaiter$1(this, void 0, void 0, function* () {
            if (!!this.currentUser) {
                data.query['idToken'] = yield this.currentUser.getIdToken();
            }
            return data;
        }))
            .extend()
            .setEndpoint(this.app.options.authEndpoint || 'auth');
        // initial change state (signin locally)
        setTimeout(() => this.signInWithLocalUser(), 1000);
    }
    onAuthStateChanged(next) {
        pubsub_2(this.SHEETBASE_USER_CHANGED, (msg, user) => next(user));
    }
    checkActionCode(code) {
        return __awaiter$1(this, void 0, void 0, function* () {
            return yield this.Api.get('/oob', { oobCode: code });
        });
    }
    createUserWithEmailAndPassword(email, password) {
        return __awaiter$1(this, void 0, void 0, function* () {
            const { info, idToken, refreshToken } = yield this.Api.put('/', {}, {
                email, password, offlineAccess: true,
            });
            const user = yield this.signIn(info, idToken, refreshToken);
            return { user };
        });
    }
    signInWithEmailAndPassword(email, password) {
        return __awaiter$1(this, void 0, void 0, function* () {
            let user;
            if (!!this.currentUser) {
                user = this.currentUser;
            }
            else {
                const { info, idToken, refreshToken } = yield this.Api.post('/', {}, {
                    email, password, offlineAccess: true,
                });
                user = yield this.signIn(info, idToken, refreshToken);
            }
            return { user };
        });
    }
    signInWithCustomToken(token) {
        return __awaiter$1(this, void 0, void 0, function* () {
            let user;
            if (!!this.currentUser) {
                user = this.currentUser;
            }
            else {
                const { info, idToken, refreshToken } = yield this.Api.post('/', {}, {
                    customToken: token, offlineAccess: true,
                });
                user = yield this.signIn(info, idToken, refreshToken);
            }
            return { user };
        });
    }
    signInAnonymously() {
        return __awaiter$1(this, void 0, void 0, function* () {
            let user;
            if (!!this.currentUser) {
                user = this.currentUser;
            }
            else {
                const { info, idToken, refreshToken } = yield this.Api.put('/', {}, {
                    offlineAccess: true,
                });
                user = yield this.signIn(info, idToken, refreshToken);
            }
            return { user };
        });
    }
    sendPasswordResetEmail(email) {
        return __awaiter$1(this, void 0, void 0, function* () {
            return yield this.Api.put('/oob', {}, { mode: 'resetPassword', email });
        });
    }
    verifyPasswordResetCode(code) {
        return __awaiter$1(this, void 0, void 0, function* () {
            return yield this.Api.get('/oob', { oobCode: code, mode: 'resetPassword' });
        });
    }
    confirmPasswordReset(code, newPassword) {
        return __awaiter$1(this, void 0, void 0, function* () {
            return yield this.Api.post('/oob', {}, {
                oobCode: code,
                mode: 'resetPassword',
                newPassword,
            });
        });
    }
    signInWithPopup(provider) {
        return __awaiter$1(this, void 0, void 0, function* () {
            if (!this.currentUser) {
                const providerConfig = !!this.app.options.authProviders ?
                    this.app.options.authProviders[provider.providerId] : null;
                if (!!providerConfig) {
                    // remember provider
                    this.oauthProvider = provider;
                    // add handler to parent window
                    window['handleOauthResult'] = (fragment) => this.handleOauthResult(fragment);
                    // process request
                    const { clientId, redirectUri } = providerConfig;
                    return createPopup({
                        url: provider.url(clientId, redirectUri || (getHost() + '/__/auth/handler')),
                    });
                }
            }
        });
    }
    googleAuthProvider() {
        return new AuthProvider('google.com', 'https://accounts.google.com/o/oauth2/v2/auth', 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile');
    }
    facebookAuthProvider() {
        return new AuthProvider('facebook.com', 'https://www.facebook.com/v3.2/dialog/oauth', 'email');
    }
    signOut() {
        return __awaiter$1(this, void 0, void 0, function* () {
            this.currentUser = null;
            // notify user change
            pubsub_1(this.SHEETBASE_USER_CHANGED, null);
            // remove user info & id token & refresh token from local
            yield this.app.Localstorage.remove(this.SHEETBASE_USER_INFO);
            yield this.app.Localstorage.remove(this.SHEETBASE_USER_CREDS);
        });
    }
    signIn(info, idToken, refreshToken) {
        return __awaiter$1(this, void 0, void 0, function* () {
            const { uid } = info;
            this.currentUser = new User(this.Api, info, idToken, refreshToken);
            // notify user change
            pubsub_1(this.SHEETBASE_USER_CHANGED, this.currentUser);
            // save user info & id token & refresh token to local
            yield this.app.Localstorage.set(this.SHEETBASE_USER_INFO, info);
            yield this.app.Localstorage.set(this.SHEETBASE_USER_CREDS, { uid, idToken, refreshToken });
            return this.currentUser;
        });
    }
    signInWithLocalUser() {
        return __awaiter$1(this, void 0, void 0, function* () {
            // retrieve local creds and info
            const creds = yield this.app.Localstorage.get(this.SHEETBASE_USER_CREDS);
            let info = yield this.app.Localstorage.get(this.SHEETBASE_USER_INFO);
            // log user in
            if (!!creds && !!info && creds.uid === info.uid) {
                let idToken = creds.idToken;
                // caching check
                const beenSeconds = (new Date().getTime() - new Date(info.lastLogin).getTime()) / 1000;
                if (beenSeconds > 86400) { // info expired, over 1 day
                    // renew idToken if expired
                    if (isExpiredJWT(idToken)) {
                        const expiredUser = new User(this.Api, info, idToken, creds.refreshToken);
                        idToken = yield expiredUser.getIdToken();
                    }
                    // fetch new info
                    info = yield this.Api.get('/user', { idToken });
                }
                // sign user in
                this.signIn(info, idToken, creds.refreshToken);
            }
            else {
                // notify initial state changed
                pubsub_1(this.SHEETBASE_USER_CHANGED, null);
            }
        });
    }
    handleOauthResult(result) {
        return __awaiter$1(this, void 0, void 0, function* () {
            // if result available
            if (result !== '') {
                // process query
                const query = result.substr(1).split('&');
                // Parse the URI hash to fetch the access token
                const credential = {};
                for (let i = 0; i < query.length; i++) {
                    const [key, value = null] = query[i].split('=');
                    let finalValue = value;
                    if (key === 'scope') {
                        finalValue = value.split('%20');
                    }
                    else if (key === 'expires_in') {
                        finalValue = +value;
                    }
                    credential[key] = finalValue;
                }
                // get user profile
                if (!credential.access_token) {
                    throw new Error('Sheetbase oauth error!'); // no access token
                }
                else {
                    return yield this.processOauthResult(credential);
                }
            }
        });
    }
    processOauthResult(credential) {
        return __awaiter$1(this, void 0, void 0, function* () {
            const { access_token: accessToken } = credential;
            const { providerId } = this.oauthProvider;
            const { info, idToken, refreshToken } = yield this.Api.get('/oauth', { providerId, accessToken });
            const user = yield this.signIn(info, idToken, refreshToken);
            return { user };
        });
    }
}

function auth(app) {
    // get the default app
    if (!app) {
        if (!window['$$$SHEETBASE_APPS']) {
            throw new Error('No app for auth component.');
        }
        app = window['$$$SHEETBASE_APPS'].getApp();
    }
    // return the instance
    if (!!app.Auth) {
        return app.Auth;
    }
    else {
        return new AuthService(app);
    }
}
window['$$$SHEETBASE_COMPONENTS'].Auth = AuthService;

export { AuthService as Auth, AuthProvider, User, auth };
//# sourceMappingURL=sheetbase-auth.js.map
