DEV Community

Cover image for Alexa with BigCommerce and Bold Subscriptions
Ablomis
Ablomis

Posted on

Alexa with BigCommerce and Bold Subscriptions

In this blogpost I will show how you can enable voice commerce for your BigCommerce store with Bold Checkout and Bold Subscriptions.

Use-case

We will start with a very simple use-case, where a user can use Alexa to check their subscription content, delivery schedule and next order date.

Prerequisites

  1. BigCommerce store
  2. Basic knowledge of BigCommerce store configuration
  3. Basic Node.js knwoledge

Setup

  1. First thing you need to do is to set up a Bold developer account. You can follow this guide
  2. Get your Bold Developer API Key using this guide. Make sure to write down both the key and the secret.
  3. Go to your BigCommerce store dashboard->Apps and install Bold Checkout from the marketplace.

Configure Bold Checkout

Install Bold Checkout from the BigCommerce marketplace. Open the Bold Checkout in the Apps section of your BigCommerce store.

  1. Set up a payment gateway. We don't want to do any real transactions, so select "Development mode" - you will have a Test Gateway set up.

Image description

  1. Set up your shipping
    Configure a shipping zone, since we will not be sending any orders yet, feel free to provide any configuration there.

  2. Set up a tax zone
    Similar to shipping, feel free to provide any configuration as we will not conduct any transactions.

  3. Enable Bold checkout
    Go to "Customize and place a test order" and click on the big blue "Enable Bold Checkout" button. Rest of the settings don't matter at this time.

You are all done and you Bold Checkout Home page should look like this (note that Payment Gateway is unchecked due to running in development mode).

This is how your home page should look like.

Image description

Configure products for your store

Make sure you have at least one product in your store. What it is and its configuration are not important.

Configure Bold Subscriptions

Install Bold Subscriptions from the BigCommerce marketplace. Open the Bold Subscriptions in the Apps section of your BigCommerce store.

  1. Go to "Subscriptions Groups" and crate a new one.
  2. Select name, frequency and other options.
  3. Go to Bold Checkout, marketplace tab and install Subscriptions v2 plugin.

You are all set.

Create subscription

To be able to manage a subscription via Alexa, you need to create it. To achieve this, all you have to do is to to complete a shopping journey as a registered or a guest customer. Note the email that you have used for this purpose, we will need it later.

Set up Alexa skill

  1. Go to your Alexa dashboard and create a new skill
  2. Provide a name for it, select "Custom" model and Node.js backend.
  3. On the next page select "Start from Scratch"

This is the code that you have now in the code tab:

/* *
 * This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).
 * Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,
 * session persistence, api calls, and more.
 * */
const Alexa = require('ask-sdk-core');

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speakOutput = 'Welcome to Bold Brew';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

const HelloWorldIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'Hello World!';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};

const HelpIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'You can say hello to me! How can I help?';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

const CancelAndStopIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent'
                || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent');
    },
    handle(handlerInput) {
        const speakOutput = 'Goodbye!';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};
/* *
 * FallbackIntent triggers when a customer says something that doesn’t map to any intents in your skill
 * It must also be defined in the language model (if the locale supports it)
 * This handler can be safely added but will be ingnored in locales that do not support it yet 
 * */
const FallbackIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.FallbackIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'Sorry, I don\'t know about that. Please try again.';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};
/* *
 * SessionEndedRequest notifies that a session was ended. This handler will be triggered when a currently open 
 * session is closed for one of the following reasons: 1) The user says "exit" or "quit". 2) The user does not 
 * respond or says something that does not match an intent defined in your voice model. 3) An error occurs 
 * */
const SessionEndedRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
    },
    handle(handlerInput) {
        console.log(`~~~~ Session ended: ${JSON.stringify(handlerInput.requestEnvelope)}`);
        // Any cleanup logic goes here.
        return handlerInput.responseBuilder.getResponse(); // notice we send an empty response
    }
};
/* *
 * The intent reflector is used for interaction model testing and debugging.
 * It will simply repeat the intent the user said. You can create custom handlers for your intents 
 * by defining them above, then also adding them to the request handler chain below 
 * */
const IntentReflectorHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest';
    },
    handle(handlerInput) {
        const intentName = Alexa.getIntentName(handlerInput.requestEnvelope);
        const speakOutput = `You just triggered ${intentName}`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};
/**
 * Generic error handling to capture any syntax or routing errors. If you receive an error
 * stating the request handler chain is not found, you have not implemented a handler for
 * the intent being invoked or included it in the skill builder below 
 * */
const ErrorHandler = {
    canHandle() {
        return true;
    },
    handle(handlerInput, error) {
        const speakOutput = 'Sorry, I had trouble doing what you asked. Please try again.';
        console.log(`~~~~ Error handled: ${JSON.stringify(error)}`);

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

/**
 * This handler acts as the entry point for your skill, routing all request and response
 * payloads to the handlers above. Make sure any new handlers or interceptors you've
 * defined are included below. The order matters - they're processed top to bottom 
 * */
exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        HelloWorldIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        FallbackIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler)
    .addErrorHandlers(
        ErrorHandler)
    .withCustomUserAgent('sample/hello-world/v1.2')
    .lambda();
Enter fullscreen mode Exit fullscreen mode

Feel free to modify the speakOutput for Alexa to greet you when you launch your skill here:

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speakOutput = 'Welcome to Bold Brew!';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};
Enter fullscreen mode Exit fullscreen mode

Building your first subscription intent

We will build an intent that allows a customer to get subscription details using voice:

  • Subscription product
  • Date of the next subscription

Configure intent in Alexa console

Now go to the Build tab and rename the HelloWorldIntent to SubscriptionDetailsIntent and change utterances so that the page looks like this.

Image description

Save model and build it. Now go back to the code tab.

Add customer id

In production environment you would want your Alexa to authenticate the customer, but we will skip it for simplicity in this guide (You can take a look at authentication in the Alexa documentation).

Instead, you will have to use Postman or another tool to make an authenticated request with the api secret to the subscriptions endpoint:
|https://api.boldcommerce.com/subscriptions/v1/shops/{shop_identifier}/customers
From the API result you will be able to get the customer_id corresponding to the email you used before. In my case it is 182121247

Add it in the beginning of the index.js of Alexa skill. We will use it later in our API requests to pull subscription details.

/* *
 * This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).
 * Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,
 * session persistence, api calls, and more.
 * */
const Alexa = require('ask-sdk-core');
const customer_id = 182121247
Enter fullscreen mode Exit fullscreen mode

Helper functions

We will create two helper functions: one to get subscription_id from customer_id and another one to get subscription details based on subscription_id.

First, let's add axios dependency for our API requests.

const axios = require('axios')

const Alexa = require('ask-sdk-core');
const customer_id = 182121247
Enter fullscreen mode Exit fullscreen mode

Don't forget to add axios to the package.json

{
  "name": "hello-world",
  "version": "1.2.0",
  "description": "alexa utility for quickly building skills",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Amazon Alexa",
  "license": "Apache License",
  "dependencies": {
    "axios": "^0.27.2",
    "ask-sdk-core": "^2.7.0",
    "ask-sdk-model": "^1.19.0",
    "aws-sdk": "^2.326.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Also we will need to store our API secret for our API requests. There are proper ways to do it with Alexa (i.e. save it in DynamoDB) but for simplicity we will store it in code. Lastly, we need to add our shop identifier which you can find in your BigCommerce store url:
https://store-shop_identifier.mybigcommerce.com/

const axios = require('axios')

const Alexa = require('ask-sdk-core');
const customer_id = 182121247
const shop_identifier = 'XXXshop_identifierXXX'
const api_secret = 'XXXYour_SecretXXX'
Enter fullscreen mode Exit fullscreen mode

Now let's create function that allows us to get subscription details. While a customer might have multiple subscriptions, for simplicity we will just pick the first one.

const getSubscriptionId = async (customer_id) => {
    const config = {
        headers: { Authorization: `Bearer ${api_secret}` }
    };
    const result = await axios.get(`https://api.boldcommerce.com/subscriptions/v1/shops/${shop_identifier}/customers/${customer_id}/subscriptions`, config);
    return result.data.subscriptions[0].id
}
Enter fullscreen mode Exit fullscreen mode

And another one to get subscription details

const getSubscriptionDetails = async (subscription_id) => {
    const config = {
        headers: { Authorization: `Bearer ${api_secret}` }
    };
    const result = await axios.get(`https://api.boldcommerce.com/subscriptions/v1/shops/${shop_identifier}/subscriptions/${subscription_id}`, config);
    return result.data;
}
Enter fullscreen mode Exit fullscreen mode

Subscription Details Intent code

We will use HelloWorldIntent as a baseline. Since we will make async API cals, we will need to change the handler to async function.

const SubscriptionDetailsIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'SubscriptionDetailsIntent';
    },
    async handle(handlerInput) {
        const id = await getSubscriptionId(customer_id)
        const subscription = await getSubscriptionDetails(id)
        const titleText = subscription["subscription"]["line_items"][0]["title"]
        const periodText = subscription["subscription"]["order_rrule_text"]
        const nextOrderDate = subscription["subscription"]["next_order_datetime"]
        const date = new Date(nextOrderDate)
        const day = date.getDate()
        const month = date.toLocaleString('default', { month: 'long' })

        const speakOutput = `You are subscribed for ${periodText} delivery of ${titleText}. Your next order date is ${month} ${day}`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};
Enter fullscreen mode Exit fullscreen mode

What we did here:

  1. We get subscription_id based on the customer_id
  2. We get subscription object based on subscription_id
  3. We extract various information from the subscription object including product name, delivery period, and next order date.
  4. Finally, we assemble everything in an output string, spoken by Alexa.

Don't forget to change the intent name at the very bottom of the index.js:

exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        SubscriptionDetailsIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        FallbackIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler)
    .addErrorHandlers(
        ErrorHandler)
    .withCustomUserAgent('sample/hello-world/v1.2')
    .lambda();
Enter fullscreen mode Exit fullscreen mode

That's it! You have enabled voice commerce for your subscriptions.

If you want to learn more about using Bold Subscription APIs make sure to visit documentation https://developer.boldcommerce.com/default/

Complete Alexa skill code:

const axios = require('axios')

const Alexa = require('ask-sdk-core');
const customer_id = 182121247
const shop_identifier = '?????'
const api_secret = '???????'

const getSubscriptionId = async (customer_id) => {
    const config = {
        headers: { Authorization: `Bearer ${api_secret}` }
    };
    const result = await axios.get(`https://api.boldcommerce.com/subscriptions/v1/shops/${shop_identifier}/customers/${customer_id}/subscriptions`, config);
    return result.data.subscriptions[0].id
}

const getSubscriptionDetails = async (subscription_id) => {
    const config = {
        headers: { Authorization: `Bearer ${api_secret}` }
    };
    const result = await axios.get(`https://api.boldcommerce.com/subscriptions/v1/shops/${shop_identifier}/subscriptions/${subscription_id}`, config);
    return result.data;
}

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speakOutput = 'Welcome to Bold Brew!';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

const SubscriptionDetailsIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'SubscriptionDetailsIntent';
    },
    async handle(handlerInput) {
        const id = await getSubscriptionId(customer_id)
        const subscription = await getSubscriptionDetails(id)
        const titleText = subscription["subscription"]["line_items"][0]["title"]
        const periodText = subscription["subscription"]["order_rrule_text"]
        const nextOrderDate = subscription["subscription"]["next_order_datetime"]
        const date = new Date(nextOrderDate)
        const day = date.getDate()
        const month = date.toLocaleString('default', { month: 'long' })

        const speakOutput = `You are subscribed for ${periodText} delivery of ${titleText}. Your next order date is ${month} ${day}`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};

const HelpIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'You can say hello to me! How can I help?';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

const CancelAndStopIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent'
                || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent');
    },
    handle(handlerInput) {
        const speakOutput = 'Goodbye!';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};
/* *
 * FallbackIntent triggers when a customer says something that doesn’t map to any intents in your skill
 * It must also be defined in the language model (if the locale supports it)
 * This handler can be safely added but will be ingnored in locales that do not support it yet 
 * */
const FallbackIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.FallbackIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'Sorry, I don\'t know about that. Please try again.';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};
/* *
 * SessionEndedRequest notifies that a session was ended. This handler will be triggered when a currently open 
 * session is closed for one of the following reasons: 1) The user says "exit" or "quit". 2) The user does not 
 * respond or says something that does not match an intent defined in your voice model. 3) An error occurs 
 * */
const SessionEndedRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
    },
    handle(handlerInput) {
        console.log(`~~~~ Session ended: ${JSON.stringify(handlerInput.requestEnvelope)}`);
        // Any cleanup logic goes here.
        return handlerInput.responseBuilder.getResponse(); // notice we send an empty response
    }
};
/* *
 * The intent reflector is used for interaction model testing and debugging.
 * It will simply repeat the intent the user said. You can create custom handlers for your intents 
 * by defining them above, then also adding them to the request handler chain below 
 * */
const IntentReflectorHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest';
    },
    handle(handlerInput) {
        const intentName = Alexa.getIntentName(handlerInput.requestEnvelope);
        const speakOutput = `You just triggered ${intentName}`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};
/**
 * Generic error handling to capture any syntax or routing errors. If you receive an error
 * stating the request handler chain is not found, you have not implemented a handler for
 * the intent being invoked or included it in the skill builder below 
 * */
const ErrorHandler = {
    canHandle() {
        return true;
    },
    handle(handlerInput, error) {
        const speakOutput = 'Sorry, I had trouble doing what you asked. Please try again.';
        console.log(`~~~~ Error handled: ${JSON.stringify(error)}`);

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

/**
 * This handler acts as the entry point for your skill, routing all request and response
 * payloads to the handlers above. Make sure any new handlers or interceptors you've
 * defined are included below. The order matters - they're processed top to bottom 
 * */
exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        SubscriptionDetailsIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        FallbackIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler)
    .addErrorHandlers(
        ErrorHandler)
    .withCustomUserAgent('sample/hello-world/v1.2')
    .lambda();
Enter fullscreen mode Exit fullscreen mode

Top comments (0)