Alexaでアカウントリンクをせずに自前のWebサイトとアカウント連携する ※特定のケースのみ

# 概要
Alexa で [アカウントリンク](https://developer.amazon.com/ja-JP/docs/alexa/account-linking/understand-account-linking.html) という機能を使うことで、「Alexaに紐付いているAmazonアカウント」と「自前で運用しているサイトの会員アカウント」を紐付け、Alexaからそのサイトに対して注文処理をかけたりすることができます。

![Alexa1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416295/6dae7645-95d1-6cf6-66a5-eaefe67859ea.png)

しかしアカウントリンクをするためには、Alexaとの会話の途中で**一回スキルを終了**して、スマホ等からアカウントリンクのための`OAuthログイン`をしてもらわなければならず、ユーザーの離脱につながってしまいます。

![Alexa2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416295/6c61f98a-0f91-51b1-6fc2-939f74cf8344.png)

しかし、ある条件を満たせば、アカウントリンクをしなくてもAlexaのAmazonアカウントと、サイトの会員アカウントを紐付けて処理をすることができます。

# 条件
* 自前のサイトで既に `Amazonログイン`・`Amazon Pay` を導入していること。
* Alexaに話しかけたユーザーが既にそのサイトで `Amazonログイン` などを一度でも実施済み。

会員情報に Amazon の `buyerID` という値を紐付けて保存していれば条件を満たします。

# アカウントリンクをせずに会員を特定する方法

Alexaはスキル起動時に端末固有の `apiAccessToken` という値を生成します。
まず、このトークンをAPIなどでサイトに渡します。
サイト側では受け取った `apiAccesToken` の値を元に、
[Amazon Pay Buyer ID API](https://pay-api.amazon.jp/live/v1/buyer/id)に問い合わせると `BuyerID` が返ってきます。
あとは会員情報に保存されている `buyerID` と照合すれば会員を特定できます。

![Alexa3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416295/5f67d195-b23a-15e3-e8ca-7be5967c94e4.png)

# Alexaのフローチャート例

すぐに離脱されないよう、アカウント判定処理はできるだけ注文処理などの直前に持ってきた方が良いでしょう。

1. Amazon Payを端末が許可しているか確認し、許可されてなければ設定を促してスキルを終了する。
2. 既にそのスキルでアカウントリンク済みか確認する。済みなら6、済みでなければ3へ。
3. サイトにapiAccessTokenを投げ、会員が特定できるか問い合わせる
4. サイト側でapiAccessTokenからBuyerIDを特定し、そのBuyerIDの会員がいるか調べ、結果を返す。
5. 会員が特定できない場合はアカウントリンクを促してスキルを終了する。
6. 特定した会員で注文処理を行う。

# 実装例

## Alexa側の処理 (Node.js)

```javascript
// Amazon Pay の Setup 処理のハンドラー
const ConnectionsSetupResponseHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'Connections.Response' && handlerInput.requestEnvelope.request.name === 'Setup';
  },

  async handle(handlerInput) {
    const actionResponsePayload    = handlerInput.requestEnvelope.request.payload;
    const actionResponseStatusCode = handlerInput.requestEnvelope.request.status.code;
    if (actionResponseStatusCode != 200) {
      const result = errorHandler.handleErrors(handlerInput);
      // Amazon Payが許可されていない場合は設定を促して終了
      if (result.permissionsError) {
        return sendAmazonPayPermissionCard(handlerInput);
      // それ以外の理由で Setup 処理がうまくいかなかった場合
      } else {
        return handlerInput.responseBuilder
          .speak(result.errorMessage)
          .withShouldEndSession(true)
          .getResponse();
      }
    }

    // Amazon Payのパーミッション情報を取得
    const permissions = handlerInput.requestEnvelope.context.System.user.permissions;
    // 注文処理を行うための、アカウントに紐付いたアクセストークンを取得
    let   accessToken = handlerInput.requestEnvelope.context.System.user.accessToken;
    
    // Amazon Payでの自動支払いに同意していない場合
    if (permissions.scopes['payments:autopay_consent'].status === 'DENIED') {
      return handlerInput.responseBuilder
       .speak('寄付を続行するためには、Amazon Payの使用権限を許可する必要があります。Alexaアプリのホーム画面に表示されている「許可リクエスト」をタップして、'
        + 'Amazon Payの使用権限を許可してください。また、Alexaアプリの「設定」から「Alexaアカウント」をタップして、'
        + '音声ショッピングの権限がオンになっていることをご確認ください。完了したら、「アレクサ、W WEBSITEを開いて」と話しかけてください。')
       .withAskForPermissionsConsentCard(['payments:autopay_consent'])
       .withShouldEndSession(true)
       .getResponse();
    // アカウントリンクの設定を行っていない場合
    } else if (!accessToken) {
      // アカウントリンク無しで注文できるか確認
      const response = await axios.post('https://~~~~/link_account', querystring.stringify({
          api_access_token: handlerInput.requestEnvelope.context.System.apiAccessToken
        }), {
        validateStatus: function (status) {
          return true;
        }
      });
      // アクセストークンが取得できなければアカウントリンクを促して終了
      if (!response.data || !response.data.access_token) {
        return handlerInput.responseBuilder
          .speak('注文を続行するためには、W WEBSITEのログインアイディーでアカウント連携を行う必要があります。'
            + 'Alexaアプリのホーム画面に表示されている「アカウントのリンク」をタップして、W WEBSITE'
            + 'に一度ログインしてください。完了したら、「アレクサ、W WEBSITEを開いて」と話しかけてください。'
            + 'W WEBSITEのアカウントをお持ちでない場合は、'
            + 'W WEBSITEのホームページでアカウントを作成いただけます。')
          .withLinkAccountCard()
          .withShouldEndSession(true)
          .getResponse();
      }
      // 会員が特定できた場合は、その会員のアクセストークンを受け取る
      accessToken = response.data.access_token;
    }

    const billingAgreementDetails = actionResponsePayload.billingAgreementDetails;
    const billingAgreementID      = billingAgreementDetails.billingAgreementId;
    
    // 注文を行う
    const response = await axios.post('https://~~/order', handlerInput.requestEnvelope.request.token, {
      headers: {
        Authorization: 'Bearer ' + accessToken
      },
      validateStatus: function (status) {
        return true;
      }
    });
    
    if (response.status !== 200) {
      return handlerInput.responseBuilder
        .speak('申し訳ありません。注文を完了することができませんでした。しばらく時間を置いてお試しください。')
        .withShouldEndSession(true)
        .getResponse();
    }

    let directiveObject = {
      type: "Connections.SendRequest",
      name: "Charge",
      payload: ~~~,
      token: ~~~
    };

    return handlerInput.responseBuilder
      .addDirective(directiveObject)
      .speak('では、決済処理を始めます')
      .withShouldEndSession(true)
      .getResponse();
  }
};
```

## サイト側の処理 (PHP)

```php
$token = $_POST['apiAccessToken'];
$ch = curl_init('https://pay-api.amazon.jp/live/v1/buyer/id');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization' => "Bearer $token"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
$buyerID = $response === null ? null : $response['buyerID'];
// 以下 buyerID から会員を特定し、アクセストークンなどを生成して返す処理
```