Alexa 音声でアカウントリンクする - Voice-Forward Account Linking -


# 前提
- スキルのモデル
    - カスタムスキルのみ
- アカウントリンク設定
    - `ユーザーが音声を使用して自分のアカウントをスキルにリンクすることを許可する`をオンにする
- ユーザー認証
    - Login with Amazon (LWA) のみ
- アクセス権限
    - `ユーザー名（姓名または名前）`をオンにする
    - `スキルのパーソナライズ`はオフにする
- ユーザープロフィール
    - ユーザーの`プロフィール`に電話番号が登録されている

# キーワード
- Account Linking
- Login with Amazon (LWA) 
- Skill Connections

# 実装手順
- プロフィールの確認
- セキュリティプロファイルの設定
- スキルの設定
- コード作成

# プロフィールの確認

アカウント所有者に対して、電話番号が登録されていないと以下のようなエラーが出て Voice-Forward Account Linking が使用できないので、まずはここを確認する。

> The user hasn't specified phone number necessary to validate their account linking request.

家族アカウントがたくさんあってどれがアカウント所有者かわからなくなってしまった場合は、:busts_in_silhouette: [プロフィール管理](https://www.amazon.co.jp/manage-your-profiles/home)画面で確認できる。プロフィールの詳細を開いて `アカウント管理者` と書いてあるアカウントを探す。また、プロフィールを作り過ぎてしまってAlexaアプリでプロフィール消せずに困っている場合はここで削除することができる。Alexaアプリでは削除ができない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/8bdfc953-320e-7e49-32a0-191f028c1c9b.png)

　<img width="30%" border="1" alt="アカウント所有者" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/3c248fc4-170d-a10d-cfb4-edf8ee4c7769.png">　<img width="30%" border="1" alt="普通のユーザー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/d586f595-d104-7a65-9749-9889909814e3.png">

不要なアカウントを消したはいいけど、Alexaアプリで以下のようなエラーがでるようになってしまったら、Alexaアプリをサインアウトすることで元にもどる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/3e954790-086d-9eb6-45a1-be9c9fa8f3e0.png)

# セキュリティプロファイルの設定

[Login With Amazon](https://developer.amazon.com/loginwithamazon/console/site/lwa/overview.html)の設定。新規作成して必要情報を入力するだけ。簡単です。

[参考：LWAでセキュリティプロファイルを作成](https://qiita.com/takara328/items/712d0db50be903c43c73#lwa%E3%81%A7%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%E3%83%97%E3%83%AD%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/3778f525-c8de-95c4-cc2b-0bb652421410.png)

# スキルの設定

Devloper Console の `ツール` － `アカウントリンク` から、`ユーザーが音声を使用して自分のアカウントをスキルにリンクすることを許可する`をオンにする。

当然`ユーザーがアカウントや既存アカウントへのリンクを作成することを許可しますか？  `もオンにしておく。
`アカウントリンクなしでスキルを有効にすることをユーザーに許可します (推奨)。`これもオンにしておく。そうしないと、Alexaアプリでスキルを有効化した時にアカウントリンクを求められるため。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/5dd5e082-eebe-d990-23c7-c9116f9df71c.png)

`Auth Code Grant` に、セキュリティプロファイルの値を設定する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/068e876a-f884-6409-d5ce-c5cbd10f175d.png)

Devloper Console の `ツール` － `アクセス権限` で、`ユーザー名（姓名または名前）`をオンにする。また、`スキルのパーソナライズ`がオンになっていると、音声によるアカウントリンクができないので、これはオフにしておく。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/e7fc92e7-7008-cb37-ab1b-b67c6d5a29ea.png)


# コード作成

設定ができていれば、サンプルコードをコピペするだけで動く。サンプルコードやステータスは以下に記載されている。

[参考：Voice-Forward Account Linking Implementation Details (LWA)](https://developer.amazon.com/ja-JP/docs/alexa/account-linking/voice-forward-account-linking-implementation-details.html)

`responseBuilder` で 下記のディレクティブを設定するだけです。下記の例は `LaunchRequestHandler` に記載していますが、アカウントリンクが必要なハンドラーの`responseBuilder`に記載すればＯＫです。`Skill Connections` の機能を利用してAmazon側に処理を投げる。

```:addDirective
const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle(handlerInput) {
        const accessToken = handlerInput.requestEnvelope.context.System.user.accessToken;
        console.log(`accessToken: ${JSON.stringify(accessToken)}`);
        if (accessToken === undefined){
            //Account is not linked
            return handlerInput.responseBuilder
            .speak('音声によるアカウントリンクを開始します。')
            .addDirective({
                'type': 'Connections.StartConnection',
                'uri': 'connection://AMAZON.AskForAccountLinking/1',
                'onCompletion': 'RESUME_SESSION',
                'input': {
                    '@version': '1',
                    '@type': 'AskForAccountLinking'
                },
                'token': 'accountLinking'
                // Token can be anything that can be used to identify the request on session resumption
            })
            .getResponse();
        }else{
            // Handle the account already linked flow.
        }
    }
};
```

Amazon側で処理が完了したら、`SessionResumedRequest` というリクエストが送信されてくるので、このリクエストを受け取るハンドラーを作っておく必要がある。これもサンプル通り。他の`Skill Connections`のリクエストと区別できるように、`request.cause.token === 'accountLinking'`を条件に加えられている。


```:SessionResumedRequest
const AccountLinkingSessionResumedHandler = {
    canHandle(handlerInput) {
        const request = handlerInput.requestEnvelope.request;
    return request.type === 'SessionResumedRequest' && request.cause.token === 'accountLinking'; // Token needs to be same as passed in request
    },
    handle(handlerInput) {
       console.log(`AccountLinkingSessionResumedHandler: ${JSON.stringify(handlerInput)}`);
       const status = handlerInput.requestEnvelope.request.cause.status;
       const code = status.code;
       const message = status.message;
       console.log(`AccountLinkingSessionResumedHandler received status code : ${code} and message : ${message}`);

       const accountLinkingStatus = handlerInput.requestEnvelope.request.cause.result.status;

       if (code !== '200'){
        //Error occured in the request
        //Response
        return handlerInput.responseBuilder
            .speak('リクエストでエラーが発生しました。')
            .withLinkAccountCard()
            .getResponse();
       }

       // Now check to see if the user passed or failed account linking.
       const accountLinkingTaskResult = handlerInput.requestEnvelope.request.cause.result;
       const accountLinkingTaskStatus = accountLinkingTaskResult.status;        
       console.log(`accountLinkingTaskStatus: ${JSON.stringify(accountLinkingTaskStatus)}`);
       if (accountLinkingTaskStatus === 'LINKED') {
           // Account was linked or is linked successfully as part of the request
           // <HANDLE_LINKED>
           //Response
       var accessToken = handlerInput.requestEnvelope.context.System.user.accessToken;

        // Logic to look up or create account if necessary
            // If you have a resource server that makes call internally to LWA to get the LWA ID
            // makeHttpCall('resource_server',accessToken);

            // If you need LWA_ID for your operation
            // https://developer.amazon.com/docs/login-with-amazon/obtain-customer-profile.html#call-profile-endpoint
            // makeHttpCall('https://api.amazon.com/user/profile?access_token=*',accessToken);
            // Use LWA_ID from the request above to look up or create an account as necessary

            // If you need customer contact information, use the Alexa Customer Profile API
            // https://developer.amazon.com/en-US/docs/alexa/custom-skills/request-customer-contact-information-for-use-in-your-skill.html

            return handlerInput.responseBuilder
                .speak('アカウントがリンクされました。')
                .getResponse();

        } else if (accountLinkingTaskStatus === 'DENIED') {
            // User denied the account linking permissions on request or sharing the information with the skill, hence the account linking flow failed.

            // <HANDLE_DENIED>
            //Response
            return handlerInput.responseBuilder
                .speak('アカウントリンクができませんでした。。')
                .withLinkAccountCard()
                .getResponse();

        } else if (accountLinkingTaskStatus === 'NOT_ANSWERED') {
      　     // The user did not answer the question, asked as part of the skill.
            // <HANDLE_NOT_ANSWERED>
            //Response
            return handlerInput.responseBuilder
                .speak('ユーザーからの答えがありませんでした。。')
                .withLinkAccountCard()
                .getResponse();
        } else if (accountLinkingTaskStatus === 'USER_REQUIREMENTS_NOT_MET'){
            // The request was cancelled because the voice account linking flow can't be triggered for the customer or can't be completed.
            // <HANDLE_USER_REQUIREMENTS_NOT_MET>
            //Response
            return handlerInput.responseBuilder
                .speak('リクエストがキャンセルされました。')
                .withLinkAccountCard()
                .getResponse();
        }

    }
};
```

こんな感じで、音声でアカウントリンクさせることができる。

# 参考
- [Voice-Forward Account Linking Implementation Details (LWA)](https://developer.amazon.com/ja-JP/docs/alexa/account-linking/voice-forward-account-linking-implementation-details.html)