【ハンズオン】Nuxt.jsとLaravelとLINE Front-end Frameworkを使ってECサイトを作ろう！

## はじめに
前回LINE Front-end Framework（LIFF）でお問い合わせフォームを作成しました。

https://qiita.com/Ryo9597/items/2665a63ac21617281b26

今回は、Nuxt.jsとLaravelを使ってECサイトを作ってみます。

## 開発環境
今回は以下の技術を採用しています。

| フロントエンド | バックエンド | インフラ | サービス |
| -------------- | ------------ | -------- | -------- |
| LIFF           | Laravel      | Docker   | HubSpot  |
| Nuxt.js        |              | Netlify  | Stripe   |
|                |              | Heroku   | MicroCMS |

Dockerは環境構築のために使用しています。
デプロイに関しては、フロントエンドはNetlifyを使い、バックエンドはHerokuを使用しています。

HubSpotは顧客管理、Stripeは決済、MicroCMSはECで販売する商品を登録するために使用しています。

本業でカスタマーサクセスをやっているのでやっぱり顧客管理はCRM使いたいよねってことでHubSpotを選択しています。

Stripeも最初は`payment-links`を使いたかったですが、コード書くなら`checkout`でいいよねってことで使いませんでした。
`payment-links`はどういうタイミングで使えばいいんでしょうね？

https://stripe.com/jp/payments/payment-links

MicroCMSは、わざわざ商品を登録するサイトを作るのがめんどくさかったので採用しました。
ECサイトを作る際にも普通に要件次第では使えるのではないかなーと感じました。

## 完成コード

https://github.com/ssk9597/LIFF-Nuxt-Laravel-EC

サイトURLはこちらです。
このサイトはLINEログインしないと使えないので、LINEログインをするためにLINEブラウザで開いてください。

https://liff.line.me/1656187153-Ko4lj5LZ

## ハンズオン！

### Githubからクローン

https://github.com/ssk9597/Docker-Laravel-Nuxt-Nginx-MySQL

GitHubにやり方は書いていますが、以下を実行することで環境構築ができます。

```terminal:ターミナル
$ git clone git@github.com:ssk9597/Docker-Laravel-Nuxt-Nginx-MySQL.git
$ cd Docker-Laravel-Nuxt-Nginx-MySQL
$ make nuxt
$ make backend
```

こちらは以下の記事を参考にしてください。

https://qiita.com/Ryo9597/items/ba0838e68da2a73bc55b

`frontend/nuxt.config.js`に関してはこのままだとエラーが発生します。
なのでここだけは以下のコードをコピペしてください。

```javascript:frontend/nuxt.config.js
require('dotenv').config();
const { API_URL } = process.env;

export default {
  head: {
    title: 'frontend',
    htmlAttrs: {
      lang: 'ja',
    },
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
  },

  css: [],

  plugins: [],

  components: true,

  buildModules: [],

  watchers: {
    webpack: {
      poll: true,
    },
  },

  modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv'],

  env: {
    API_URL,
  },

  proxy: {
    '/api': process.env.API_URL,
  },

  axios: {
    baseURL: process.env.API_URL,
    browserBaseURL: process.env.API_URL,
  },

  build: {},
};
```

### NetlifyとHerokuにデプロイを行う
#### Netlify
以下の記事で詳細まで説明されているのでこちらを見てもらえれば問題なくできるかと思います。

https://qiita.com/isihigameKoudai/items/e3b136e9964f1d30d73d

また、ステージング環境に関してもやっておいた方がいいです。

https://blog.microcms.io/netlify-deploy-contexts/

#### Heroku
Herokuのデプロイは結構躓きがちなので自分が参考にした資料を載せておきます。

今回は複数の`buildpack`が必要になるので以下を参考にしましょう。
ほとんどこちらの資料だけでできるはずです。

https://innovator-japan.hatenablog.com/entry/2018/10/22/131853

`Papertrail`でログも出しておきたいのでこれもやっておきましょう。
LIFFはフロント側でログを出すには、`alert`しか使えないですし、`alert`にオブジェクトなどが表示できないなど制限があります。
なので、Papertrailを使ってバックエンド側でログを出しておくのがいいかと思います。

https://blog.hrendoh.com/setup-logging-to-papertrail-for-laravel6-on-heroku/

最後に通信できているか確認しましょう。
以下の記事の`Axios`の部分を追加しましょう。

https://qiita.com/Ryo9597/items/ba0838e68da2a73bc55b#pagesindexvue%E3%81%AE%E4%BF%AE%E6%AD%A3%E3%81%A7%E6%A4%9C%E8%A8%BC

これで`Hello World!`が表示されればOKです。

### サイト内で使用する画像を準備する

私は以下の２サイトから持ってきました。

https://storyset.com/

https://o-dan.net/ja/

まぁ正直なんでもいいです。

### LINE Developersでチャネルを作成する
#### 作成手順
①プロバイダーの作成
②新規チャネルの作成
③LIFFの作成
- - -  

プロバイダーを作成します。
![スクリーンショット 2021-08-01 8.42.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/4026c8fe-bef5-4666-b4f8-2276c9f6bd96.png)

プロバイダー名はご自身のお名前でいいかと思います。
![スクリーンショット 2021-08-01 8.43.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/98c8d760-c4bb-7451-35d5-421aaade4774.png)

新規チャネルを作成します。
![スクリーンショット 2021-08-01 8.41.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/034e13ae-4a06-16a8-84e8-75f354b96cc6.png)

LINEログインを選択してください。
![スクリーンショット 2021-08-01 8.47.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/470dd892-606a-0efc-80e8-c8eb937cbd11.png)

設定を行います。
ウェブページにLINEログインを組み込むので、アプリタイプはウェブアプリにしておいてください。
![スクリーンショット 2021-08-01 8.49.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/06331582-ab7e-be55-91a9-5bd2ed2ba363.png)

ではLIFFアプリを追加しましょう！
![スクリーンショット 2021-08-01 8.54.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/ec2424a6-44b1-d35d-7694-c39d63e75cbf.png)

基本情報の設定は以下のように行います。
![スクリーンショット 2021-08-01 8.56.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/825f6b07-1d4d-f743-ef5c-73d892ad183a.png)

これでLIFFアプリの作成ができました！
![スクリーンショット 2021-08-01 9.04.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/7454f0ff-2267-eec0-7a1c-55bf4eb64d73.png)


### Netlifyに.envを登録する
先ほど作成したLIFFアプリの`チャネルID`と`LIFF ID`を`.env`に登録します。

![スクリーンショット 2021-08-01 8.52.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/14bf82bd-9c1a-de86-bceb-954f09e65312.png)

![スクリーンショット 2021-08-01 9.04.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/7e44c69b-7a4f-ef2b-1a1c-5f838de711ed.png)

それでは登録を行います。

![スクリーンショット 2021-08-01 9.07.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/b340946a-e336-a74c-c6dc-22888abe0650.png)

### LIFFアプリのタイトルを設定する
サイトタイトルをつけますが、こちらは任意の名前でOKです。
私は、`LIFF's Shop`にしています。

```diff_javascript:frontend/nuxt.config.js
require('dotenv').config();
const { API_URL } = process.env;

export default {
  target: 'static',
  head: {
+   title: "LIFF's Shop",
    htmlAttrs: {
      lang: 'ja',
    },
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
  },

  plugins: [],

  components: true,

  buildModules: [],

  watchers: {
    webpack: {
      poll: true,
    },
  },

  modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv', '@nuxtjs/style-resources'],

  env: {
    API_URL,
  },

  proxy: {
    '/api': process.env.API_URL,
  },

  axios: {
    baseURL: process.env.API_URL,
    browserBaseURL: process.env.API_URL,
  },

  build: {},
};
```

### LIFFアプリにLIFF SDKを組み込む
`npm`パッケージもあるのですがうまくいかないことも多いのでSDKを使います。

https://developers.line.biz/ja/docs/liff/developing-liff-apps/#integrating-sdk

```diff_javascript:frontend/nuxt.config.js
require('dotenv').config();
const { API_URL } = process.env;

export default {
  target: 'static',
  head: {
    title: "LIFF's Shop",
    htmlAttrs: {
      lang: 'ja',
    },
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
+   script: [{ src: 'https://static.line-scdn.net/liff/edge/2/sdk.js' }],
  },

  plugins: [],

  components: true,

  buildModules: [],

  watchers: {
    webpack: {
      poll: true,
    },
  },

  modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv', '@nuxtjs/style-resources'],

  env: {
    API_URL,
  },

  proxy: {
    '/api': process.env.API_URL,
  },

  axios: {
    baseURL: process.env.API_URL,
    browserBaseURL: process.env.API_URL,
  },

  build: {},
};
```

### `.env`のLIFF_IDなどを`nuxt.config.js`で読み取れるようにする
先ほどNetlifyに登録した`env`をフロントエンドで使えるようにしましょう！

```diff_javascript:frontend/nuxt.config.js
require('dotenv').config();
+ const { API_URL, LIFF_ID, LIFF_CHANNEL_ID, MICROCMS_API_KEY } = process.env;

export default {
  target: 'static',
  head: {
    title: "LIFF's Shop",
    htmlAttrs: {
      lang: 'ja',
    },
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
    script: [{ src: 'https://static.line-scdn.net/liff/edge/2/sdk.js' }],
  },

  css: [{ src: '@/assets/styles/style.scss', lang: 'scss' }],

  styleResources: {
    scss: ['@/assets/styles/style.scss'],
  },

  plugins: [],

  components: true,

  buildModules: [],

  watchers: {
    webpack: {
      poll: true,
    },
  },

  modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv', '@nuxtjs/style-resources'],

  env: {
    API_URL,
    LIFF_ID,
+   LIFF_CHANNEL_ID,
+   MICROCMS_API_KEY,
  },

  proxy: {
    '/api': process.env.API_URL,
  },

  axios: {
    baseURL: process.env.API_URL,
    browserBaseURL: process.env.API_URL,
  },

  build: {},
};
```

### リセットCSSを導入する
いろいろ選択肢がありますが私は、`css-wipe`を使っています。

https://github.com/stackcss/css-wipe

html5doctorのHTML5 Resetをベースに作られたスタイルシートです。
「box-sizing: border-box;」が定義されており、すべてのmargin, padding, borderが消されています。

http://html5doctor.com/html-5-reset-stylesheet/

導入方法はめちゃくちゃ簡単です。

```terminal:ターミナル
$ npm install css-wipe --save
```

`layout`のCSSに1行入れるだけでOKです。

```diff_vue:frontend/layouts/default.vue
<style lang="scss">
+   @import 'css-wipe';
</style>
```

### エラー発生
ここまでをコミットした場合以下のエラーが発生するはずです。
発生しなかった人はそのままスルーで！

`Node Sass version 6.0.1 is incompatible with ^4.0.0 || ^5.0.0.`

「node-sassのバージョン6はたけーぞ、4か５に下げろや」と怒られるはずなので、対応しましょう。

```terminal:ターミナル
$ npm uninstall -D node-sass
$ npm install -D node-sass@5.0.0
```

これでOKです。
再度コミットしてください！
多分エラーは消えるはずです。

### LIFFアプリを初期化する
liff.init()メソッドを実行すると、LIFFアプリが初期化され、LIFFアプリからLIFF SDKのほかのメソッドを実行できるようになります。

https://developers.line.biz/ja/docs/liff/developing-liff-apps/#developing-liff-apps

アクセスした際に初期化されるようにしたいので、pages/index.vue内で読み込むことにしましょう。

```diff_vue:frontend/pages/index.vue
<template>
  <div>
  </div>
</template>

<script>
export default {
+ async mounted() {
+   await liff.init({
+     liffId: process.env.LIFF_ID,
+   });
+ },
};
</script>
```

### まずはフロントエンド側のデザインをやっていきましょう！
完成予想図は以下のような感じです。

![LIFF_Nuxt_Laravel_EC.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/dd1dd1e1-2f74-20f9-d743-4b22bf504808.png)

構成としては4つのブロックになっています。

| ブロック |
| -------------- |
| ヘッダー       |
| ヒーロー       |
| 会員登録       |
| 商品           |

必要なコンポーネントです。

| Atoms        | Molecules    | Template | 
| ------------ | ------------ | -------- | 
| FailAlert    | Header       | Cancel   | 
| Heading      | Hero         | Index    | 
| LinkButton   | Registration | Success  |
| SuccessAlert |              |          |

それでは必要なコンポーネントを作りつつ、そのコンポーネントをなぜ使用するのかも解説していきます。

ただその前に今回は変数定義用のscssファイルを作りましょう。
今回は使用する色に限りがあるのでこちらをscssで管理していきます。

```scss:frontend/assets/styles/utility/_color.scss
$color_black: #2a2b2f;
$color_white: #fff;
$color_deep_gray: #979797;
$color_gray: #bfbfbe;
$color_yellow: #f5c500;
$color_success: #4caf51;
$color_fail: #e57472;
```

```scss:frontend/assets/styles/style.scss
@import "utility/color";
```

では最後に`nuxt.config.js`に読み込みましょう。

```diff_javascript:frontend/nuxt.config.js
require('dotenv').config();
const { API_URL, LIFF_ID, LIFF_CHANNEL_ID } = process.env;

export default {
  target: 'static',
  head: {
    title: "LIFF's Shop",
    htmlAttrs: {
      lang: 'ja',
    },
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
    script: [{ src: 'https://static.line-scdn.net/liff/edge/2/sdk.js' }],
  },

+ css: [{ src: '@/assets/styles/style.scss', lang: 'scss' }],

+ styleResources: {
+   scss: ['@/assets/styles/style.scss'],
+ },

  plugins: [],

  components: true,

  buildModules: [],

  watchers: {
    webpack: {
      poll: true,
    },
  },

  modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv', '@nuxtjs/style-resources'],

  env: {
    API_URL,
    LIFF_ID,
    LIFF_CHANNEL_ID,
  },

  proxy: {
    '/api': process.env.API_URL,
  },

  axios: {
    baseURL: process.env.API_URL,
    browserBaseURL: process.env.API_URL,
  },

  build: {},
};
```

これでOKです。
それではコンポーネントの作成に移ります！

**⚠️注意点**
**完成形のコードをすべて記載しました。**
**API側のやり取りなど解説は後ほど行うこととします。**
なのでこちらはすべてコピペしていただければOKです。

#### FailAlert.vue
##### なぜ作るのか
こちらは会員登録に失敗したときに表示されるアラートです。
要件としては３秒間表示されて勝手に消えるようにします。

##### では作っていきます
```vue:frontend/components/Atoms/FailAlert.vue
<template>
  <div class="alert">
    <p class="alert-title">ログインに失敗しました</p>
  </div>
</template>

<script>
export default {};
</script>

<style lang="scss" scoped>
.alert {
  position: fixed;
  z-index: 100;
  background: $color_fail;
  width: 100%;
  max-width: 390px;
  height: 50px;
  display: flex;
  justify-content: center;
  align-items: center;

  &-title {
    font-size: 12px;
    color: $color_white;
  }
}
</style>
```

#### Heading.vue
##### なぜ作るのか
見出しとして使用します。

##### では作っていきます
```vue:frontend/components/Atoms/Heading.vue
<template>
  <div class="heading">
    <h2 class="heading-main">{{ main }}</h2>
    <p class="heading-sub">{{ sub }}</p>
  </div>
</template>

<script>
export default {
  props: {
    main: {
      type: String,
      required: true,
    },
    sub: {
      type: String,
      required: true,
    },
  },
};
</script>

<style lang="scss" scoped>
.heading {
  margin: 60px 0 30px 0;
  text-align: center;
  &-main {
    font-size: 32px;
    margin-bottom: 10px;
  }

  &-sub {
    font-size: 16px;
  }
}
</style>
```

#### LinkButton.vue
##### なぜ作るのか
ページ移動を伴うボタンです。
TOPページに戻るために使用します。

##### では作っていきます
```vue:frontend/components/Atoms/LinkButton.vue
<template>
  <div class="button">
    <nuxt-link to="/" class="button-btn">トップに戻る</nuxt-link>
  </div>
</template>

<script>
export default {};
</script>

<style lang="scss" scoped>
.button {
  margin: 40px auto 0 auto;
  width: 200px;
  height: 40px;
  background: $color_yellow;
  border-color: transparent;
  &-btn {
    line-height: 40px;
    font-size: 18px;
    color: $color_black;
    text-decoration: none;
    font-weight: bold;
  }
}
</style>
```

#### SuccessAlert.vue
##### なぜ作るのか
こちらは会員登録に成功したときに表示されるアラートです。
要件としては３秒間表示されて勝手に消えるようにします。

##### では作っていきます
```vue:frontend/components/Atoms/SuccessAlert.vue
<template>
  <div class="alert">
    <p class="alert-title">ログインに成功しました</p>
  </div>
</template>

<script>
export default {};
</script>

<style lang="scss" scoped>
.alert {
  position: fixed;
  z-index: 100;
  background: $color_success;
  width: 100%;
  max-width: 390px;
  height: 50px;
  display: flex;
  justify-content: center;
  align-items: center;

  &-title {
    font-size: 12px;
    color: $color_white;
  }
}
</style>
```

これで`Atoms`は完成です。
次に、`Molecules`を作成していきます。

#### Header.vue
##### なぜ作るのか
サイトのヘッダーデザインです。

##### では作っていきます
```vue:frontend/components/Molecules/Header.vue
<template>
  <header class="header">
    <p class="header-title">LIFF's Shop</p>
  </header>
</template>

<script>
export default {};
</script>

<style lang="scss" scoped>
.header {
  position: fixed;
  background: $color_black;
  width: 100%;
  max-width: 390px;
  height: 50px;
  display: flex;
  justify-content: center;
  align-items: center;

  &-title {
    font-size: 20px;
    color: $color_white;
    font-weight: bold;
  }
}
</style>
```

#### Hero.vue
##### なぜ作るのか
サイトのヒーローデザインです。

##### では作っていきます
```vue:frontend/components/Molecules/Hero.vue
<template>
  <div>
    <div class="blank-space"></div>
    <div class="hero">
      <img class="hero-img" src="@/assets/images/top.png" alt="ヒーロー" />
    </div>
  </div>
</template>

<script>
export default {};
</script>

<style lang="scss" scoped>
.blank-space {
  padding-top: 50px;
}

.hero {
  height: 300px;

  &-img {
    width: 390px;
    height: 300px;
  }
}
</style>
```

#### Registration.vue
##### なぜ作るのか
会員登録のデザインです。
こちらには会員登録したユーザーむけに、データの活用方法に関して記載しています。
こちらの記述をすることで、LIFFログインでメールアドレスを取得することができます。

##### では作っていきます
```vue:frontend/components/Molecules/Registration.vue
<template>
  <div class="registration">
    <Heading :main="'Registration'" :sub="'会員登録'" />
    <div class="registration-container">
      <p class="registration-container-text">
        本Webサービスでは、ログイン時の認証画面にて許可を頂いた場合のみ、あなたのLINEアカウントに登録されているメールアドレスを取得します。
      </p>
      <p class="registration-container-text">
        取得したメールアドレスは、HubSpotにユーザー登録するIDとして利用する目的以外では使用いたしません。また、法令に定められた場合を除き、第三者への提供はいたしません。
      </p>
      <div class="registration-container-image">
        <img src="@/assets/images/line.png" alt="LINEログイン" @click="childLineLogin" />
      </div>
    </div>
  </div>
</template>

<script>
// components
import Heading from '@/components/Atoms/Heading';
export default {
  components: {
    Heading,
  },
  props: {
    lineLogin: {
      type: Function,
      required: true,
    },
  },
  methods: {
    childLineLogin() {
      this.lineLogin();
    },
  },
};
</script>

<style lang="scss" scoped>
.registration-container {
  width: 250px;
  margin: 0 auto;
  background: $color_white;
  padding: 20px;

  &-text {
    font-size: 0.5rem;
    line-height: 1.5;
    padding-bottom: 10px;
  }

  &-image {
    padding-top: 20px;
    text-align: center;
  }
}
</style>
```

##### LINEログインの時に「ユーザーのメールアドレス」を取得できるようにしましょう
ということでここまでをgithubに、コミット、プッシュしましょう。
そしてこの情報取り扱いに関する記述をした部分をスクショしましょう！

それでは、LINEにメールアドレス取得権限を付与してもらいましょう。
![スクリーンショット 2021-08-01 10.05.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/e9c46fa5-912f-4665-58fc-fbf086351259.png)

結構すぐに権限がもらえるはずです。
画像認識等をしているんでしょうね〜〜

「申請済み」になったらOKなので、スコープを付与しましょう。

![スクリーンショット 2021-08-01 10.07.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/b5746d0b-d2e5-1f7b-15b3-9d62073bc55f.png)

これでLINEログインの際にメールアドレスが取得できるようになりました！

これで`Molecules`は完成です。
次に、`Template`を作成していきます。

#### Cancel.vue
##### なぜ作るのか
Stripeの決済を行う際に、キャンセルページが必要なため作成します。

##### では作っていきます
```vue:frontend/components/Template/Cancel.vue
<template>
  <div class="container">
    <Header />
    <div class="message">
      <p class="message-text">商品購入に失敗しました。</p>
      <p class="message-text">もう一度ご確認ください。</p>
      <LinkButton />
    </div>
  </div>
</template>

<script>
// components
import Header from '@/components/Molecules/Header';
import LinkButton from '@/components/Atoms/LinkButton';
export default {
  components: {
    Header,
    LinkButton,
  },
};
</script>

<style lang="scss">
.container {
  margin: 0 auto;
  background: $color_gray;
  height: 100vh;
  width: 100%;
  max-width: 390px;
  padding-bottom: 60px;
}
.message {
  padding-top: 70px;

  &-text {
    padding: 0 10px;
    line-height: 1.5;
  }
}
</style>
```

では、キャンセルページの作成をしましょう。

```vue:frontend/pages/cancel.vue
<template>
  <div>
    <Cancel />
  </div>
</template>

<script>
// components
import Cancel from '@/components/Template/Cancel';
export default {
  components: {
    Cancel,
  },
};
</script>
```

#### Index.vue
##### なぜ作るのか
TOPページです。
こちらでは、MicroCMSに登録した商品を受け取る処理を記載する必要があります。
なので、まずはMicroCMSの商品登録を行いましょう。

##### MicroCMSへ商品登録を行う
まずはアカウントを作成しましょう。

https://microcms.io/

作成方法に関することは公式サイトのブログがわかりやすいのでこちらを参考にされてください。

https://blog.microcms.io/microcms-nuxt-jamstack-blog

下記のようなスキーマを作成していきます。
![スクリーンショット 2021-08-01 10.40.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/060aeafe-85a1-f117-8000-5539993e70b4.png)

Stripeの商品料金API IDは後ほど取得しますので今は適当な値を埋めておきましょう。

ということでこんな感じで3つ程度商品登録をしましょう！

![スクリーンショット 2021-08-01 10.42.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/dcaf869d-90c4-8676-c614-0c59e3bd6848.png)

APIキーも必要になるので取得してNetlifyに登録しましょう。

![スクリーンショット 2021-08-01 11.24.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/8b1edf44-b39b-1a49-c47a-2fe83fa209bd.png)

では登録しましょう。

![スクリーンショット 2021-08-01 11.25.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/83d01d38-5b72-9770-a294-b94f0c8f4240.png)


##### では作っていきます
MicroCMSのデータを取得していきましょう。
`asyncData`は`pages`ディレクトリでないとできないのでまずは`pages/index.vue`を作成します。

```diff_vue:frontend/pages/index.vue
<template>
  <div>
    <Index :products="products" />
  </div>
</template>

<script>
// components
import Index from '@/components/Template/Index';

export default {
  components: {
    Index,
  },
+ async asyncData({ $axios }) {
+   const products = await $axios.$get('https://liff-nuxt-laravel.microcms.io/api/v1/product', {
+     headers: { 'X-API-KEY': process.env.MICROCMS_API_KEY },
+   });
+   return { products };
  },
  data() {
    return {
      products: '',
    };
  },
  async mounted() {
    await liff.init({
      liffId: process.env.LIFF_ID,
    });
  },
};
</script>
```

では、Templateを作成します。

```vue:frontend/components/Template/Index.vue
<template>
  <div class="container">
    <div v-if="isSuccess">
      <SuccessAlert />
    </div>
    <div v-if="isFail">
      <FailAlert />
    </div>
    <Header />
    <Hero />
    <Registration :lineLogin="lineLogin" />
    <div class="product">
      <Heading :main="'Product'" :sub="'商品'" />
      <div class="card">
        <div class="card-container" v-for="product in products.contents" :key="product.id">
          <img class="card-product-img" :src="product.image.url" />
          <p class="card-product-name">{{ product.name }}</p>
          <p class="card-product-price">¥{{ product.price }}</p>
          <div class="button">
            <button v-if="idToken" class="button-btn" @click="checkout(product)">購入する</button>
            <button v-else class="button-btn disabled" disabled>購入する</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
// components
import Heading from '@/components/Atoms/Heading';
import SuccessAlert from '@/components/Atoms/SuccessAlert';
import FailAlert from '@/components/Atoms/FailAlert';
import Header from '@/components/Molecules/Header';
import Hero from '@/components/Molecules/Hero';
import Registration from '@/components/Molecules/Registration';

export default {
  components: {
    Heading,
    SuccessAlert,
    FailAlert,
    Header,
    Hero,
    Registration,
  },
  props: {
    products: {
      type: Object,
      required: true,
    },
  },
  data() {
    return {
      idToken: '',
      isSuccess: false,
      isFail: false,
    };
  },
  updated() {
    setTimeout(() => {
      this.isSuccess = false;
    }, 3000);
    setTimeout(() => {
      this.isFail = false;
    }, 3000);
  },
  methods: {
    async lineLogin() {
      try {
        // IDトークン
        this.idToken = liff.getIDToken();
        if (this.idToken) {
          await this.$axios.$post('/hubspot/store', {
            idToken: this.idToken,
          });
          this.isSuccess = true;
        } else {
          this.isFail = true;
        }
      } catch (err) {
        alert(err);
        alert(err.response.data.error);
        alert(err.response.data.error_description);
      }
    },
    async checkout(product) {
      const url = await this.$axios.$post('/stripe/store', {
        productStripePriceApi: product.stripe_price_api,
        idToken: this.idToken,
      });
      if (url) {
        window.location.href = url;
      }
    },
  },
};
</script>

<style lang="scss">
.container {
  margin: 0 auto;
  background: $color_gray;
  width: 100%;
  max-width: 390px;
  padding-bottom: 60px;
}

.card {
  &-container {
    border-radius: 10px;
    background: $color_white;
    width: 250px;
    height: 300px;
    margin: 0 auto 30px auto;
  }

  &-product {
    &-img {
      border-radius: 10px 10px 0 0;
      width: 250px;
      height: 150px;
    }

    &-name {
      padding: 20px 0 0 20px;
    }

    &-price {
      padding: 20px 0 20px 20px;
      font-weight: bold;
    }
  }
}

.button {
  text-align: center;
  &-btn {
    display: inline-block;
    width: 200px;
    height: 40px;
    font-size: 18px;
    font-weight: bold;
    background: $color_yellow;
    border-color: transparent;

    &.disabled {
      color: $color_deep_gray;
      background: $color_gray;
    }
  }
}
</style>
```

本当はCardの部分もコンポーネント化してあげる方がいいのですが今回はデータのやり取りがめんどくさかったので諦めましたw

#### Success.vue
##### なぜ作るのか
Stripeの決済を行う際に、サクセスページが必要なため作成します。

##### では作っていきます
```vue:frontend/components/Template/Success.vue
<template>
  <div class="container">
    <Header />
    <div class="message">
      <p class="message-text">ご購入ありがとうございます！</p>
      <p class="message-text">ご不明点がありましたらお気軽にご連絡ください。</p>
      <LinkButton />
    </div>
  </div>
</template>

<script>
// components
import Header from '@/components/Molecules/Header';
import LinkButton from '@/components/Atoms/LinkButton';
export default {
  components: {
    Header,
    LinkButton,
  },
};
</script>

<style lang="scss">
.container {
  margin: 0 auto;
  background: $color_gray;
  height: 100vh;
  width: 100%;
  max-width: 390px;
  padding-bottom: 60px;
}
.message {
  padding-top: 70px;

  &-text {
    padding: 0 10px;
    line-height: 1.5;
  }
}
</style>
```

では、サクセスページの作成をしましょう。

```vue:frontend/pages/success.vue
<template>
  <div>
    <Success />
  </div>
</template>

<script>
// components
import Success from '@/components/Template/Success';
export default {
  components: {
    Success,
  },
};
</script>
```

これでフロントエンドの開発は終了です。

ではこの後はLINEログインをやっていきましょう。

### LINEログインでプロフィールを取得する
LIFFでのログインは、`liff.getDecodedIDToken()`が一番簡単です。
これで名前もメールアドレスもすべて取得することができます。

https://developers.line.biz/ja/reference/liff/#get-id-token

しかし、**ユーザー情報をサーバーに送信しないでください**とあるようにセキュリティの問題があります。

ということで、以下のように行います。

| フロントエンド               | バックエンド                                                         |
| ---------------------------- | -------------------------------------------------------------------- |
| ①IDトークンを取得           |                                                                      |
| ②IDトークンをサーバーに渡す |                                                                      |
|                              | ③IDトークンを検証して、ユーザーのプロフィールを取得する |

https://developers.line.biz/ja/docs/liff/using-user-profile/#sending-id-token

ということでやっていきましょう。

#### ①IDトークンを取得
IDトークンの取得方法は簡単です。

https://developers.line.biz/ja/reference/liff/#get-id-token

```diff_vue:frontend/components/Template/Index.vue
<template>
  <div class="container">
    <Registration :lineLogin="lineLogin" />
  </div>
</template>

<script>
// components
import Registration from '@/components/Molecules/Registration';

export default {
  components: {
    Registration,
  },
  data() {
    return {
      idToken: '',
    };
  },
  methods: {
    async lineLogin() {
      try {
        // IDトークン
+       this.idToken = liff.getIDToken();
      } catch (err) {
        alert(err);
        alert(err.response.data.error);
        alert(err.response.data.error_description);
      }
    },
  },
};
</script>
```

```vue:frontend/components/Molecules/Registration.vue
<template>
  <div class="registration">
    <Heading :main="'Registration'" :sub="'会員登録'" />
    <div class="registration-container">
      <p class="registration-container-text">
        本Webサービスでは、ログイン時の認証画面にて許可を頂いた場合のみ、あなたのLINEアカウントに登録されているメールアドレスを取得します。
      </p>
      <p class="registration-container-text">
        取得したメールアドレスは、HubSpotにユーザー登録するIDとして利用する目的以外では使用いたしません。また、法令に定められた場合を除き、第三者への提供はいたしません。
      </p>
      <div class="registration-container-image">
        <img src="@/assets/images/line.png" alt="LINEログイン" @click="childLineLogin" />
      </div>
    </div>
  </div>
</template>

<script>
// components
import Heading from '@/components/Atoms/Heading';
export default {
  components: {
    Heading,
  },
  props: {
    lineLogin: {
      type: Function,
      required: true,
    },
  },
  methods: {
    childLineLogin() {
      this.lineLogin();
    },
  },
};
</script>
```

#### ②IDトークンをサーバーに渡す
取得したIDトークンをLaravelに渡してあげます。

```diff_vue:frontend/components/Template/Index.vue
<template>
  <div class="container">
    <div class="product">
      <Heading :main="'Product'" :sub="'商品'" />
      <div class="card">
        <div class="card-container" v-for="product in products.contents" :key="product.id">
          <img class="card-product-img" :src="product.image.url" />
          <p class="card-product-name">{{ product.name }}</p>
          <p class="card-product-price">¥{{ product.price }}</p>
          <div class="button">
            <button v-if="idToken" class="button-btn" @click="checkout(product)">購入する</button>
            <button v-else class="button-btn disabled" disabled>購入する</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
// components
import Heading from '@/components/Atoms/Heading';

export default {
  components: {
    Heading,
  },
  props: {
    products: {
      type: Object,
      required: true,
    },
  },
  data() {
    return {
      idToken: '',
    };
  },
  methods: {
    async lineLogin() {
      try {
        // IDトークン
        this.idToken = liff.getIDToken();
+       if (this.idToken) {
+         await this.$axios.$post('/hubspot/store', {
+           idToken: this.idToken,
+         });
+       }
      } catch (err) {
        alert(err);
        alert(err.response.data.error);
        alert(err.response.data.error_description);
      }
    },
  },
};
</script>
```

#### ③IDトークンを検証して、ユーザーのプロフィールを取得する
プロフィールを取得するには、`ルーティング`と`コントローラー`の2つが必要になります。

##### ルーティング
```diff_php:api/routes/api.php
<?php

use Illuminate\Http\Request;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
*/

Route::get("/", function () {
  return "Hello World!";
});

// HubSpotにユーザーを登録する
+ Route::post("/hubspot/store", "HubSpotController@store");
```

##### コントローラー
コントローラーを作成しましょう。

```terminal:ターミナル
$ php artisan make:controller HubSpotController --resource
```

また、`Guzzle`を使用するので`Guzzle`をインストールしましょう。

https://github.com/guzzle/guzzle

```terminal:ターミナル
$ composer require guzzlehttp/guzzle
```


次に、`.env`を使うので`config`からアクセスさせましょう。
`env()`を使ってはいけない理由などは以下の記事を見てみてください。

https://hiroto-k.hatenablog.com/entry/2018/03/28/213000

```php:api/config/env.php
<?php

return [
  "line_client_id" => env("LINE_CLIENT_ID"),
];
```

また、Herokuの`.env`の値も確認しておきましょう。

![スクリーンショット 2021-08-01 11.48.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/99b2a20e-115f-1def-77f3-2bc96d8a4a31.png)

`.env`の値をコントローラー内で取得しましょう。

```diff_php:api/app/Http/Controllers/HubSpotController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use GuzzleHttp\Client;

// Log
use Illuminate\Support\Facades\Log;

class HubSpotController extends Controller
{
  public function store(Request $request)
  {
    try {
      // request
      $idToken = $request->input("idToken");
      Log::info($idToken);

      // env
+     $clientID = config("env.line_client_id");
      Log::info($clientID);
    } catch (\GuzzleHttp\Exception\BadResponseException $e) {
      Log::info($e);
      return $e->getResponse()->getBody()->getContents();
    }
  }
}
```

では、IDトークンを検証してユーザーのプロフィールとメールアドレスを取得しましょう！

https://developers.line.biz/ja/reference/line-login/#revoke-access-token

```diff_php:api/app/Http/Controllers/HubSpotController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use GuzzleHttp\Client;

// Log
use Illuminate\Support\Facades\Log;

class HubSpotController extends Controller
{
  public function store(Request $request)
  {
    try {
      // request
      $idToken = $request->input("idToken");
      Log::info($idToken);

      // env
      $clientID = config("env.line_client_id");
      Log::info($clientID);

      // guzzle
+     $client = new Client();
+     $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [
+       "form_params" => [
+         "id_token" => $idToken,
+         "client_id" => $clientID
+       ]
+     ]);
    } catch (\GuzzleHttp\Exception\BadResponseException $e) {
      Log::info($e);
      return $e->getResponse()->getBody()->getContents();
    }
  }
}
```

では次にこの取得した名前やメールアドレスを`HubSpot`に登録させましょう。

まずはHubSpotのAPIを取得しましょう。

![スクリーンショット 2021-08-01 11.58.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/23824422-0a2e-511d-323c-8a99ecc07710.png)

このAPIをHerokuの`.env`に入力しましょう。

![スクリーンショット 2021-08-01 12.06.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/815c11ea-8835-baf4-c118-7a862247bf35.png)

次に、`.env`を使うので`config`からアクセスさせましょう。

```diff_php:api/config/env.php
<?php

return [
  "line_client_id" => env("LINE_CLIENT_ID"),
+ "hubspot_api_key" => env("HUBSPOT_API_KEY"),
];
```

最後にコントローラーを作成します。
LINEログイン情報を使用してHubSpotのコンタクトを作成するので、`POST
/crm/v3/objects/contacts`を使います。

https://developers.hubspot.jp/docs/api/crm/contacts

今回は、`cURL`コマンドで作成します。

```diff_php:api/app/Http/Controllers/HubSpotController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use GuzzleHttp\Client;

// Log
use Illuminate\Support\Facades\Log;

class HubSpotController extends Controller
{
  public function store(Request $request)
  {
    try {
      // request
      $idToken = $request->input("idToken");

      // env
      $clientID = config("env.line_client_id");
+     $HubSpotApiKey = config("env.hubspot_api_key");

      // guzzle
      $client = new Client();
      $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [
        "form_params" => [
          "id_token" => $idToken,
          "client_id" => $clientID
        ]
      ]);

      // HubSpot Value
+     $profile = json_decode($response->getBody()->getContents(), true);
+     $profileName = $profile["name"];
+     $profileEmail = $profile["email"];
      // Log::info($profile["name"]);
      // Log::info($profile["email"]);

      // Register HubSpot
+     $data = array(
+       "properties" => [
+         "email" => $profileEmail,
+         "firstname" => $profileName
+       ]
+     );

+     $formParams = json_encode($data, JSON_UNESCAPED_UNICODE);
      // Log::info($formParams);

      // cURL
+     $curl = curl_init();
+     curl_setopt_array($curl, array(
+       CURLOPT_URL => "https://api.hubapi.com/crm/v3/objects/contacts?hapikey=" . $HubSpotApiKey,
+       CURLOPT_RETURNTRANSFER => true,
+       CURLOPT_ENCODING => "",
+       CURLOPT_MAXREDIRS => 10,
+       CURLOPT_TIMEOUT => 30,
+       CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
+       CURLOPT_CUSTOMREQUEST => "POST",
+       CURLOPT_POSTFIELDS => $formParams,
+       CURLOPT_HTTPHEADER => array(
+         "accept: application/json",
+         "content-type: application/json"
+       ),
+     ));
+     $response = curl_exec($curl);
+     $err = curl_error($curl);
+     curl_close($curl);

+     if ($err) {
+       Log::info($err);
+     } else {
+       Log::info($response);
+     }
    } catch (\GuzzleHttp\Exception\BadResponseException $e) {
      Log::info($e);
      return $e->getResponse()->getBody()->getContents();
    }
  }
}
```

これでHubSpotにユーザー登録が可能になりました。
それでは最後にStripeを使って決済を行いましょう。

### `Stripe`で決済を作成する

Stripeで決済を作成するには以下のように進めていきます。

| フロントエンド                                       | バックエンド                                             | サービス                  |
| ---------------------------------------------------- | -------------------------------------------------------- | ------------------------- |
|                                                      |                                                          | ①Stripeに商品登録を行う  |
|                                                      |                                                          | ②StripeのAPIを取得する |
| ③IDトークンとStripeの商品IDをバックエンドに渡す |                                                          |                           |
|                                                      | ④IDトークンを検証して、ユーザーのプロフィールを取得する |                           |
|                                                      | ⑤Stripeで決済処理を行う                                 |                           |
|                                                      | ⑥決済画面をフロントエンドに渡す                        |                           |
| ⑦決済画面へページ移動させる                         |                                                          |                           |

#### ①Stripeに商品登録を行う
「商品を追加」から商品を登録します。

![スクリーンショット 2021-08-01 12.29.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/bd37ba71-7780-887f-a01e-518a825d9f38.png)

商品詳細ページに`API ID`があるのでこちらを`Micro CMS`に入力しましょう。

![スクリーンショット 2021-08-01 12.30.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/4227ac85-189c-30ef-f4b5-a1f04971221a.png)

#### ②StripeのAPIを取得する
![スクリーンショット 2021-08-01 12.33.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/25bab422-9d8f-21a2-dd0e-b5c299d92ce2.png)

APIが取得できたらこれをHerokuに登録します。

![スクリーンショット 2021-08-01 12.35.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/0af0dc8a-2265-2332-b475-01a92f17036d.png)

これでOKです！

#### ③IDトークンとStripeの商品IDをバックエンドに渡す
```diff_vue:frontend/components/Template/Index.vue
<template>
  <div class="container">
    <div class="product">
      <Heading :main="'Product'" :sub="'商品'" />
      <div class="card">
        <div class="card-container" v-for="product in products.contents" :key="product.id">
          <img class="card-product-img" :src="product.image.url" />
          <p class="card-product-name">{{ product.name }}</p>
          <p class="card-product-price">¥{{ product.price }}</p>
          <div class="button">
            <button v-if="idToken" class="button-btn" @click="checkout(product)">購入する</button>
            <button v-else class="button-btn disabled" disabled>購入する</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
// components
import Heading from '@/components/Atoms/Heading';

export default {
  components: {
    Heading,
  },
  props: {
    products: {
      type: Object,
      required: true,
    },
  },
  data() {
    return {
      idToken: '',
    };
  },
  methods: {
    async lineLogin() {
      try {
        // IDトークン
        this.idToken = liff.getIDToken();
        if (this.idToken) {
          await this.$axios.$post('/hubspot/store', {
            idToken: this.idToken,
          });
        }
      } catch (err) {
        alert(err);
        alert(err.response.data.error);
        alert(err.response.data.error_description);
      }
    },
+   async checkout(product) {
+     const url = await this.$axios.$post('/stripe/store', {
+       productStripePriceApi: product.stripe_price_api,
+       idToken: this.idToken,
+     });
+   },
  },
};
</script>
```

#### ④IDトークンを検証して、ユーザーのプロフィールを取得する
これは先ほどやったことと全く同じですね。
まずはルーティングから作成しましょう。

```diff_php:api/routes/api.php
<?php

use Illuminate\Http\Request;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
*/

Route::get("/", function () {
  return "Hello World!";
});

// HubSpotにユーザーを登録する
Route::post("/hubspot/store", "HubSpotController@store");

// Stripeを使って決済を行う
+ Route::post("/stripe/store", "PayloadController@store");
```

では次にコントローラーの作成をしましょう。

```terminal:ターミナル
$ php artisan make:controller PayloadController --resource
```

コントローラーの記述をしていきます。

```diff_php:api/app/Http/Controllers/PayloadController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
// guzzle
use GuzzleHttp\Client;

class PayloadController extends Controller
{
  public function store(Request $request)
  {
    // line
+   $idToken = $request->input("idToken");

    // env
+   $clientID = config("env.line_client_id");

    // guzzle
+   $client = new Client();
+   $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [
+     "form_params" => [
+       "id_token" => $idToken,
+       "client_id" => $clientID
+     ]
+   ]);

+   $profile = json_decode($response->getBody()->getContents(), true);
+   $profileEmail = $profile["email"];
  }
}
```

#### ⑤Stripeで決済処理を行う
Stripeで決済処理を行うには、`checkout`を使用します。
公式サイトに具体的なやり方が書いてあるのでこちらに倣ってやっていきます。

https://stripe.com/docs/checkout/integration-builder

##### まずはライブラリをインストールします
```terminal:ターミナル
$ composer require stripe/stripe-php
```

##### checkoutセッションを作成する

```diff_php:api/app/Http/Controllers/PayloadController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
// stripe
use Stripe\Stripe;
use Stripe\Checkout\Session;
// guzzle
use GuzzleHttp\Client;

class PayloadController extends Controller
{
  public function store(Request $request)
  {
    // $request
    // line
    $idToken = $request->input("idToken");
    // product
+   $productStripePriceApi = $request->input("productStripePriceApi");

    // env
    $clientID = config("env.line_client_id");
+   $stripeApiKey = config("env.stripe_api_key");

    // guzzle
    $client = new Client();
    $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [
      "form_params" => [
        "id_token" => $idToken,
        "client_id" => $clientID
      ]
    ]);

    $profile = json_decode($response->getBody()->getContents(), true);
    $profileEmail = $profile["email"];

    // API
+   Stripe::setApiKey($stripeApiKey);

    // header
+   header('Content-Type: application/json');

    // domain
+   $domain = "https://liff-nuxt-laravel-ec.netlify.app";

    // checkout
+   $checkout_session = Session::create([
+     'customer_email' => $profileEmail,
+     'payment_method_types' => ['card'],
+     'line_items' => [[
+       'price' => $productStripePriceApi,
+       'quantity' => 1,
+     ]],
+     'mode' => 'payment',
+     'success_url' => $domain . '/success',
+     'cancel_url' => $domain . '/cancel',
+   ]);
  }
}
```

#### ⑥決済画面をフロントエンドに渡す
公式サイトではリダイレクト処理で決済画面へ移動させていますが、
ページ移動などはフロントエンドで処理したいのでURLをフロントエンド側に渡しましょう。

```diff_php:api/app/Http/Controllers/PayloadController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
// stripe
use Stripe\Stripe;
use Stripe\Checkout\Session;
// guzzle
use GuzzleHttp\Client;

class PayloadController extends Controller
{
  public function store(Request $request)
  {
    // $request
    // line
    $idToken = $request->input("idToken");
    // product
    $productStripePriceApi = $request->input("productStripePriceApi");

    // env
    $clientID = config("env.line_client_id");
    $stripeApiKey = config("env.stripe_api_key");

    // guzzle
    $client = new Client();
    $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [
      "form_params" => [
        "id_token" => $idToken,
        "client_id" => $clientID
      ]
    ]);

    $profile = json_decode($response->getBody()->getContents(), true);
    $profileEmail = $profile["email"];

    // API
    Stripe::setApiKey($stripeApiKey);

    // header
    header('Content-Type: application/json');

    // domain
    $domain = "https://liff-nuxt-laravel-ec.netlify.app";

    // checkout
    $checkout_session = Session::create([
      'customer_email' => $profileEmail,
      'payment_method_types' => ['card'],
      'line_items' => [[
        'price' => $productStripePriceApi,
        'quantity' => 1,
      ]],
      'mode' => 'payment',
      'success_url' => $domain . '/success',
      'cancel_url' => $domain . '/cancel',
    ]);

    // return redirect($checkout_session->url, 303, [], true);
+   return $checkout_session->url;
  }
}
```

#### ⑦決済画面へページ移動させる
これで決済画面ページへ移動されます。

```diff_vue:frontend/components/Template/Index.vue
<template>
  <div class="container">
    <div class="product">
      <Heading :main="'Product'" :sub="'商品'" />
      <div class="card">
        <div class="card-container" v-for="product in products.contents" :key="product.id">
          <img class="card-product-img" :src="product.image.url" />
          <p class="card-product-name">{{ product.name }}</p>
          <p class="card-product-price">¥{{ product.price }}</p>
          <div class="button">
            <button v-if="idToken" class="button-btn" @click="checkout(product)">購入する</button>
            <button v-else class="button-btn disabled" disabled>購入する</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
// components
import Heading from '@/components/Atoms/Heading';

export default {
  components: {
    Heading,
  },
  props: {
    products: {
      type: Object,
      required: true,
    },
  },
  data() {
    return {
      idToken: '',
    };
  },
  methods: {
    async lineLogin() {
      try {
        // IDトークン
        this.idToken = liff.getIDToken();
        if (this.idToken) {
          await this.$axios.$post('/hubspot/store', {
            idToken: this.idToken,
          });
        }
      } catch (err) {
        alert(err);
        alert(err.response.data.error);
        alert(err.response.data.error_description);
      }
    },
    async checkout(product) {
      const url = await this.$axios.$post('/stripe/store', {
        productStripePriceApi: product.stripe_price_api,
        idToken: this.idToken,
      });
+     if (url) {
+       window.location.href = url;
+     }
    },
  },
};
</script>
```

これで完成です！
お疲れ様でした^ ^

## 終わりに
せっかくCRMのHubSpotを入れたので、商品購入のログなども残せるようにしたいなーと思います。
この辺も今後チャレンジしていきたいと思います。

また、HubSpotにはLINE接続するアプリもあるので同様の機能が自分自身で準備できるかも試してみたいです。

https://www.littlehelp.co.jp/ja/

ここまで読んでいただきありがとうございました！