monetization.js

/* Copyright 2024 Schibsted Products & Technology AS. Licensed under the terms of the MIT license.
 * See LICENSE.md in the project root.
 */

'use strict';

import { assert, isStr, isNonEmptyString, isUrl } from './validate.js';
import { urlMapper } from './url.js';
import { ENDPOINTS, NAMESPACE } from './config.js';
import EventEmitter from 'tiny-emitter';
import RESTClient from './RESTClient.js';
import Cache from './cache.js';
import * as spidTalk from './spidTalk.js';
import SDKError from './SDKError.js';
import version from './version.js';

const globalWindow = () => window;

/**
 * Provides features related to monetization
 */
export class Monetization extends EventEmitter {
    /**
     * @param {object} options
     * @param {string} options.clientId - Mandatory client id
     * @param {string} [options.redirectUri] - Redirect uri
     * @param {string} options.sessionDomain - Example: "https://id.site.com"
     * @param {string} [options.env=PRE] - Schibsted account environment: `PRE`, `PRO` or `PRO_NO`
     * @param {object} [options.window]
     * @throws {SDKError} - If any of options are invalid
     */
    constructor({ clientId, redirectUri, env = 'PRE', sessionDomain, window = globalWindow() }) {
        super();
        spidTalk.emulate(window);
        // validate options
        assert(isNonEmptyString(clientId), 'clientId parameter is required');

        this.cache = new Cache(() => window && window.sessionStorage);
        this.clientId = clientId;
        this.env = env;
        this.redirectUri = redirectUri;
        this._setSpidServerUrl(env);

        if (sessionDomain) {
            assert(isUrl(sessionDomain), 'sessionDomain parameter is not a valid URL');
            this._setSessionServiceUrl(sessionDomain);
        }
    }

    /**
     * Set SPiD server URL
     * @private
     * @param {string} url
     * @returns {void}
     */
    _setSpidServerUrl(url) {
        assert(isStr(url), `url parameter is invalid: ${url}`);
        this._spid = new RESTClient({
            serverUrl: urlMapper(url, ENDPOINTS.SPiD),
            defaultParams: { client_id: this.clientId, redirect_uri: this.redirectUri },
        });
    }

    /**
     * Set session-service domain
     * @private
     * @param {string} domain - real URL — (**not** 'PRE' style env key)
     * @returns {void}
     */
    _setSessionServiceUrl(domain) {
        assert(isStr(domain), `domain parameter is invalid: ${domain}`);
        const client_sdrn = `sdrn:${NAMESPACE[this.env]}:client:${this.clientId}`;
        this._sessionService = new RESTClient({
            serverUrl: domain,
            log: this.log,
            defaultParams: { client_sdrn, redirect_uri: this.redirectUri, sdk_version: version  },
        });
    }

    /**
     * Checks if the user has access to a set of products or features.
     * @param {array} productIds - which products/features to check
     * @param {number} userId - id of currently logged in user
     * @throws {SDKError} - If the input is incorrect, or a network call fails in any way
     * (this will happen if, say, the user is not logged in)
     * @returns {Object|null} The data object returned from Schibsted account (or `null` if the user
     * doesn't have access to any of the given products/features)
     */
    async hasAccess(productIds, userId) {
        if (!this._sessionService) {
            throw new SDKError(`hasAccess can only be called if 'sessionDomain' is configured`);
        }
        if (!userId) {
            throw new SDKError(`'userId' must be specified`);
        }
        if (!Array.isArray(productIds)) {
            throw new SDKError(`'productIds' must be an array`);
        }

        const sortedIds = productIds.sort();
        const cacheKey = this._accessCacheKey(productIds, userId);
        let data = this.cache.get(cacheKey);
        if (!data) {
            data = await this._sessionService.get(`/hasAccess/${sortedIds.join(',')}`);
            const expiresSeconds = data.ttl;
            this.cache.set(cacheKey, data, expiresSeconds * 1000);
        }

        if (!data.entitled) {
            return null;
        }
        this.emit('hasAccess', { ids: sortedIds, data });
        return data;
    }

    /**
     * Removes the cached access result.
     * @param {array} productIds - which products/features to check
     * @param {number} userId - id of currently logged in user
     * @returns {void}
     */
    clearCachedAccessResult(productIds, userId) {
        this.cache.delete(this._accessCacheKey(productIds, userId));
    }

    /**
     * Compute "has access" cache key for the given product ids and user id.
     * @param {array} productIds - which products/features to check
     * @param {number} userId - id of currently logged in user
     * @returns {string}
     * @private
     */
    _accessCacheKey(productIds, userId) {
        return `prd_${productIds.sort()}_${userId}`;
    }

    /**
     * Get the url for the end user to review the subscriptions
     * @param {string} [redirectUri=this.redirectUri]
     * @return {string} - The url to the subscriptions review page
     */
    subscriptionsUrl(redirectUri = this.redirectUri) {
        assert(isUrl(redirectUri), `subscriptionsUrl(): redirectUri is invalid`);
        return this._spid.makeUrl('account/subscriptions', { redirect_uri: redirectUri });
    }

    /**
     * Get the url for the end user to review the products
     * @param {string} [redirectUri=this.redirectUri]
     * @return {string} - The url to the products review page
     */
    productsUrl(redirectUri = this.redirectUri) {
        assert(isUrl(redirectUri), `productsUrl(): redirectUri is invalid`);
        return this._spid.makeUrl('account/products', { redirect_uri: redirectUri });
    }
}

export default Monetization;