2
\$\begingroup\$

My Android app must request my Cloud Functions server to create in-app products at runtime ; then, the server responds with the created one-time product. I've written the server Cloud Function that creates the product. Here is how it works (full minimal, executable code is embedded at the end of the question):

  • My function uses JavaScript, Node.JS (notable modules: request, uuid/v4), firebase-functions, firebase-admin, admin.firestore()

  • It checks that the Android app's user is allowed to request my server to create an in-app product, i.e. if the user exists and has the adequat rights

    • (Note that I use Firebase Auth, and Firebase Firestore with Security Rules defined and enabled)

`

if(!context.auth) {
        throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated.');
    }

admin_firestore.collection('users').document(context.auth.uid).get().then(documentSnapshot => {  // Is the client allowed to ask for the creation of a managed product?
    if(!documentSnapshot.exists) {
        throw new functions.https.HttpsError('failed-precondition', 'The function must be called with an existing account.');
    }

    if(documentSnapshot.getString('app') != 'main') {
        throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated with your main account.');
    }

    if(documentSnapshot.getBoolean('deleted')) {
        throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated with an undeleted main account.');     
    }

`

  • It checks that the in-app product can be created, i.e. if it does not already exist

`

request.get({url: remote_url}, function(error, response, body) {  // Does a managed product with this SKU already exist? (querying Google Play Developer API Inapproducts.get) 

        if(error) {
            throw new functions.https.HttpsError('unknown', error);
        }

        if(response.statusCode == 200) {
            throw new functions.https.HttpsError('already-exists',  'A managed product with the SKU I\'ve generated myself already exists. You should query me again.');
        }


        if(response.statusCode != 404) {
            throw new functions.https.HttpsError('unkown',  'Something went wrong. Status code : ' + response.statusCode + '.');
        }

`

  • It generates the required in-app product's details throughout the function (depending on the previous checks)

`

const managed_product_details = {  // See https://developers.google.com/android-publisher/api-ref/inappproducts#resource
            packageName: package_name, 
            sku: sku, 
            status: status, 
            purchaseType: purchase_type,
            defaultPrice: {
                currency: currency,
                priceMicros: price_micros 
            },
            listings: {
                'en-US': {
                    title: title,
                    description: description 
                }
            },
            defaultLanguage: default_language
        };

`

  • It inserts (i.e. creates) the in-app product, providing Google Cloud Billing these details

`

request.post({url: remote_url, json: managed_product_details}, function(error, response, body) {    // Creating the managed product (querying Google Play 
                                                            // Developer API Inapproducts.insert) 
            if(error) {
                throw new functions.https.HttpsError('unknown', error);
            }

            if(response.statusCode != 200) {
                throw new functions.https.HttpsError('unkown',  'Something went wrong. Status code : ' + response.statusCode + '.');
            }

            const json_managed_product = body;  // See https://developers.google.com/android-publisher/api-ref/inappproducts/insert
            return json_managed_product; 
        }); 

`

I am asking for a review that covers these three points:

  1. Error-handling.

    • In the request.get callbacks, I check if an error occurred (request.get({url: remote_url}, function(error, response, body) { if (error) { throw new functions.https.HttpsError('unknown', error); } }) . I didn't find anything more pertinent than the code unkown (see https://firebase.google.com/docs/reference/functions/functions.https.HttpsError) . Is it a good practice to throw unkown with the error sent by request?
    • The previous link is the Google doc about HttpsError. It says:

      Make sure to throw this exception at the top level of your function and not from within a callback, as that will not necessarily terminate the function with this exception.

    • So do you know if it's good to throw new HttpsError(code, my_message) within the request and admin-firestore callbacks as it's done, for the moment?

  2. Security

    • I don't really have precise questions about security. Do you think my function is unsecure and if yes, why? Concerning the network: normally, Android apps communicate with Cloud Functions servers in HTTPS so data are encrypted (because app would use the Firebase Functions SDK's getHttpsCallable() method).
  3. Asynchronous

    • Since I use request, I use callbacks: do you think there are cases for which my function won't completely be executed (e.g. errors not thrown, code sections that should be executed but they are not, etc.) ?

Full, minimal, executable code

To execute it: deploy this Cloud Function, then e.g. use your Android app's Firebase Functions client SDK to request the Cloud Functions server (that's off-topic, so please see https://firebase.google.com/docs/functions/callable :) )

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const request = require('request');
const uuidv4 = require('uuid/v4');

/**
 * Creates a managed product (either a SEAL, or a slot).
 * Call it at runtime in an Android application, e.g. using Firebase Functions' SDK.
 * @param type the type of the managed product (either "seal", or "slot")
 * @response json_managed_product the created (inserted) one-time managed product
 *
 * WORKFLOW OVERVIEW
 * 1. Generates the following managed product's features:
 *  - SKU (built on the parametrized type)
 *  - package_name (constant)
 *  - title (built on parametrized type)
 *  - description (built on parametrized type)
 *  - currency (constant)
 *  - price_micros (constant)
 *  - status (constant)
 *  - purchase_type (constant)
 *  - default_language (constant)
 * NB: the generation of these features is stopped whether a managed product with the parametrized SKU already exists (see 2.).
 *
 * 2. Checks that a managed product with the parametrized SKU does not already exist.
 *  2.1. true: the server sends an error and stops.
 *  2.2. false: the server continues the generation of the managed product's features and its creation.
 *
 * 3. The server creates (inserts) the managed product in Firebase.
 *  3.1. error: the server sends an error and stops.
 *  3.2. success: the server sends the generated SKU
 */
exports.createManagedProduct = functions.https.onRequest(async(req, res) => {

    if(!context.auth) {
        throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated.');
    }

    const admin_firestore = admin.firestore();
    const type = req.query.type;
    const uuid_with_underscores_only = uuidv4().split('-').join('_');
    const sku = type + '_' + uuid_with_underscores_only;
    const package_name = 'com.XXX';
    const remote_url = 'https://www.googleapis.com/androidpublisher/v3/applications/' + package_name + '/inappproducts/' + sku;

    admin_firestore.collection('users').document(context.auth.uid).get().then(documentSnapshot => {  // Is the client allowed to ask for the creation of a managed product?
        if(!documentSnapshot.exists) {
            throw new functions.https.HttpsError('failed-precondition', 'The function must be called with an existing account.');
        }

        if(documentSnapshot.getString('app') != 'main') {
            throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated with your main account.');
        }

        if(documentSnapshot.getBoolean('deleted')) {
            throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated with an undeleted main account.');     
        }

        const allowed_types = ['seal', 'slot'];
        if(!allowed_types.includes(type)) {
            throw new functions.https.HttpsError('invalid-argument', 'The function must be called with an allowed type of managed product.');
        }

        request.get({url: remote_url}, function(error, response, body) {  // Does a managed product with this SKU already exist? (querying Google Play Developer API Inapproducts.get) 

            if(error) {
                throw new functions.https.HttpsError('unknown', error);
            }

            if(response.statusCode == 200) {
                throw new functions.https.HttpsError('already-exists',  'A managed product with the SKU I\'ve generated myself already exists. You should query me again.');
            }


            if(response.statusCode != 404) {
                throw new functions.https.HttpsError('unkown',  'Something went wrong. Status code : ' + response.statusCode + '.');
            }

            let title = 'A ';
            let description = '';
            switch(type) {
                case 'seal':
                    const upper_cased_title = type.toUpperCase();
                    title += upper_cased_title;
                    description = 'Contains the slots in which your Instagram publications will be placed.';
                    break;
                case 'slot':
                    title += type;
                    description = 'Contains your Instagram publication.';
                    break;
            }   
            const currency = 'EUR';
            const price_micros = 40e6;
            const status = 'active';
            const purchase_type = 'managedUser';
            const default_language = 'en-US';
            const managed_product_details = {  // See https://developers.google.com/android-publisher/api-ref/inappproducts#resource
                packageName: package_name, 
                sku: sku, 
                status: status, 
                purchaseType: purchase_type,
                defaultPrice: {
                    currency: currency,
                    priceMicros: price_micros 
                },
                listings: {
                    'en-US': {
                        title: title,
                        description: description 
                    }
                },
                defaultLanguage: default_language
            };
            const remote_url = 'https://www.googleapis.com/androidpublisher/v3/applications/' + package_name + '/inappproducts';

            request.post({url: remote_url, json: managed_product_details}, function(error, response, body) {    // Creating the managed product (querying Google Play 
                                                                // Developer API Inapproducts.insert) 
                if(error) {
                    throw new functions.https.HttpsError('unknown', error);
                }

                if(response.statusCode != 200) {
                    throw new functions.https.HttpsError('unkown',  'Something went wrong. Status code : ' + response.statusCode + '.');
                }

                const json_managed_product = body;  // See https://developers.google.com/android-publisher/api-ref/inappproducts/insert
                return json_managed_product; 
            }); 

        });

    }); 
});
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.