15
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Vue.js+Firebase+Stripeをさわさわしてみるー最終回

Posted at

その8
ようやくstripeを実装していきます。今回実装するのはチェックアウト。
サブスクリプションではなく単発購入のほうです。え?サブスクを実装したい?なら他の記事をみて。。。

購入同線は前回作成したマイページに設けてみましょう。まずはstripeのライブラリをインストールしてきます。今回使うのはvue-checkoutとよばれるやつです。

npm install vue-checkout --save

インストールが終わったら、stripeでサクッとアカウントを作成しておいてください。クレジットカードの申請不要でテストできますのでご安心くださいませ。アカウントを作成したら、APIキーを確認し、以下のようにmain.jsに記載してください。

main.js
import VueStripeCheckout from 'vue-stripe-checkout'

const options = {
  key: 'pk_xxxxxxxxxxxxxxxxxxxxxxxxx',
  image: 'https://cdn.meme.am/images/100x100/15882140.jpg',
  locale: 'auto',
  currency: 'PHP',
  billingAddress: false,
  panelLabel: 'Subscribe {{amount}}'
}
Vue.use(VueStripeCheckout, options)

key以外はひとまず適当でOKです。続いてmypage.vueを以下のように変えます。

views/Mypage.vue
<template>
  <div class="mypage">
  <br>
    <p><img :src="imgurl" width="100">
    <br>{{userid}}
    
    <div class="form-group uploadForm">
        <input type="file" class="form-control" @change="selectFile">
        <button type="submit" class="btn btn-outline-success" v-on:click="upload">アイコン変更</button>
        <div id="errArea"> {{ infoMsg }}</div>
    </div>
    </p>

    <h2>購入状態</h2>
    <br />
    <div v-if="paystatus == 1">
        paiduser \(^o^
    </div>
    <div v-else>
        <at-button @click="stripecheckout">かってくれるの?</at-button>
    </div>
    <br />


    <br>
    <h2>お気に入り</h2>
    <ul>
    <li v-for="like in likelist"><img :src="like.data().place_imageurl" width="80">
    <br>{{like.data().place_name}}
    <router-link :to="{ name: 'place', params: { place_id: like.data().place_id }}">詳細</router-link>
    | <a v-on:click="removelike(like.id)">削除</a>
    </li>
    </ul>

  </div>
</template>

<script>
import firebase from 'firebase'
import 'firebase/firestore'

export default {
  name: 'Mypage',
  data () {
    return {
      userid: '',
      commentlist: [],
      likelist: [],
      imgurl: '',
      paystatus: 0,
      infoMsg: '',
      useremail: ''
    }
  },
  created: function(){
    const uid = firebase.auth().currentUser.uid
    const email = firebase.auth().currentUser.email
    this.userid = uid
    this.useremail = email
    firebase.firestore().collection('like').where('user_id','==',uid).get().then((querySnapshot) => {
    querySnapshot.forEach((doc) => {
        this.likelist.push(doc)
    })
  })
    firebase.firestore().collection('user').where('userid','==',uid).get().then((querySnapshot) => {
    querySnapshot.forEach((doc) => {
        this.paystatus = doc.data().status
        const ref = firebase.storage().ref().child(doc.data().imgpath);
        ref.getDownloadURL().then((url) => {
        this.imgurl = url
        console.log(url)
        })
    })
  })
  },
  methods: {
    removelike: function (likeid) {
    firebase.firestore().collection("like").doc(likeid).delete().then(function() {
            console.log(likeid)
        }).catch(function(error) {
            console.error("Error removing document: ", error)
        })
    },
    stripecheckout: function () {
      this.$checkout.open({
        email: this.useremail,
        image: 'https://i.imgur.com/1PHlmFF.jpg',
        locale: 'ja',
        currency: 'JPY',
        name: 'おねがい',
        description: 'かってちょ',
        amount: 1,
        panelLabel: 'おねだん {{amount}} ',
        token: (token) => {
          this.tokeninfo = token.id
          this.paystatus = 1
          firebase.firestore().collection('payment').add({
                user_id: this.userid,
                buy_day: firebase.firestore.FieldValue.serverTimestamp(),
                token: this.tokeninfo
            }).catch(function (error) {
                console.error('Error adding document: ', error);
          })
          firebase.firestore().collection('user').where('userid','==',this.userid)
          .get().then(function(querySnapshot) {
            querySnapshot.forEach(function(doc) {
              firebase.firestore().collection('user').doc(doc.id).update({
                stripe_buy_status: 1,
                expire_day: firebase.firestore.FieldValue.serverTimestamp()
              })
            })
          })
        }
      })
    },
    selectFile: function (e) {
            e.preventDefault();
            let files = e.target.files;
            this.uploadFile = files[0];
    },
    upload: function () {
            if (!this.uploadFile) {
                this.infoMsg = '画像ファイルを選択してください'
                return;
            }
            const uid = firebase.auth().currentUser.uid
            const mail = firebase.auth().currentUser.email
            const storepath = 'tmp/' + uid + '/' + this.uploadFile.name
            var storageRef = firebase.storage().ref().child(storepath);
            storageRef.put(this.uploadFile).then(function (snapshot) {
              firebase.firestore().collection('user').where('userid','==',uid).get().then(function(doc) {
                doc.forEach(function(d) {
                console.log(d.data())
                console.log(storepath)
                if (d) {
                  firebase.firestore().collection('user').doc(d.id).update({
                    imgpath: storepath
                  })
                  console.log("チェンジ img!:")
                } else {
                  firebase.firestore().collection('user').add({
                      imgpath: storepath,
                      userid: uid,
                      usermail: mail
                  })
                  console.log("新規画像登録!");
                }
                })
              })
            })
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  margin: 0 10px;
}
a {
  color: #42b983;
}
input {
  margin: 10px 0;
  padding: 10px;
}
h2 {
    background:#1c4b7a;
    color:#fff;
}
</style>

うーん。かなり汚いコードですねーでも今回は気にしないことにしましょうw
methodにstripecheckoutを作成しました。templateにも購入ボタンを設けております。

stripecheckoutのメソッドが問題なく通ると、tokenをゲットできます。これは「payment」のコレクションを作ってそこに保存しておきましょう。なんかおかしなことが起こってたらpaymentにあるtokenをもとにstripeでいろいろチェックできます。

this.paystatus = 1

tokenをfirebaseに保存したら、コレクションuserの中にある購入状態のフラグを変更させます。今回は「stripe_buy_status」が0か1かで購入状態を判定しています。1が購入状態です。
もし、購入に期限をつけるのであればexpire_dayをここで持たせておきましょう。

なお、購入商品のレパートリーが増えてくるようであれば、購入管理用のコレクションを設けるとよいですね。

tokenが返ってきてから諸々の処理を走らせましょう。支払いが確認できるまでは動かざるごと山のごとく。

ここまででタイトルの「Vue.js+Firebase+Stripe」は終わっているのですが、せっかくなのでもう少し情報量を増やしたいと思います。yelpさんのAPIをお借りし、placeの周辺レストランを表示してみたいと思います。

まずはyelpでAPIの利用申請を行いましょう。
https://qiita.com/Yorozuyasan/items/dca15612fef1c1220cc4
とかも参考にどうぞ。

続いて、axiosと呼ばれるHTTPクライアントのパッケージをインストールします。いやはや、こいつがとても便利なんです。あと、yelpの検索に使うyelp-fusionもインストールしておきましょう。

npm install axios --save
npm install vue-axios --save
npm install yelp-fusion --save

インストールできたらmain.jsにそれぞれ追記しておいてください。

main.js
import axios from 'axios'
import VueAxios from 'vue-axios'

yelp-fusionは、vueコンポーネントの中で引っ張ってくることにしましょう。

続いて画面を作りこんでいきます。Placerest.vueファイルを作成し、以下のように書いてください。

views/Placerest.vue
<template>
  <div class="place">
    <h2>{{place_name}}周辺レストラン</h2>
    <p>{{place_lat}} | {{place_lon}}</p>
      <button @click="getyelp">getyelp</button>

        <ul>
            <li v-for="post in items" >
            <h3>{{post.alias}}</h3>
            <h4>{{post.location.address1}} {{post.location.address2}}</h4>
            <p><img :src="post.image_url" width="250"><br>
                <a v-bind:href="post.url" target="_blank">詳細</a>
            </p>
            </li>
        </ul>
  </div>
</template>

<script>
import firebase from 'firebase'
import 'firebase/firestore'
import axios from 'axios'

export default {
  name: 'placerest',
  data () {
    return {
      items: [],
      place_name: '',
      place_lat: 1.1,
      place_lon: 1.1
    }
  },
  created: function(){
      this.place_name = this.$route.params.place_name
      this.place_lat = this.$route.params.place_lat
      this.place_lon = this.$route.params.place_lon
  },
    methods: {
      getyelp: function () {
      const yelp = require('yelp-fusion');
      // Place holder for Yelp Fusion's API Key. Grab them
      // from https://www.yelp.com/developers/v3/manage_app
      const apiKey = 'xxxxxxxxxxxyyyyyyyyyyyyzzzzzzz';
      const searchRequest = {
        latitude: 35.681167, //this.$route.params.place_lon をいれたいですね
        longitude: 139.767052 //this.$route.params.place_name をいれたいですね
      };
      const client = yelp.client(apiKey);
      client.search(searchRequest).then(response => {
        const Result = response.jsonBody
        console.log(Result.businesses);
        this.items = Result.businesses
      }).catch(e => {
        console.log(e);
      })
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  margin: 0 10px;
}
a {
  color: #42b983;
}
input {
  margin: 10px 0;
  padding: 10px;
}
.vdp-datepicker{
    text-align:center;
}
h2 {
    background:#1c4b7a;
    color:#fff;
}
.tag {
  padding:5px;
  margin:1px;
  background: skyblue;
  color:#fff;
  border-radius:3px;
  font-size:10px;
}
li {
  text-align:left;
  margin-bottom:10px;
}
</style>

一先ず、logitudeとlatitudeについては、適当な値を入れています(東京駅だったかな?)
この数値については、この画面に遷移する前にパラメータで受け取っておきましょう。

この画面に遷移する方法は。Place.vueから遷移させたいと思います。ので、Place.vueを少し書き換えましょう。

views/Place.vue
<template>
  <div class="place">
    <h2>{{place_name}}</h2>
    <p>{{place_address}}</p>
    <p>{{place_wiki}}</p>
    <p><img :src="place_imageurl" width="200"></p>

    <br />
        <router-link :to="{ name: 'placerest', params: { place_id: place_id, place_name: place_name, place_lat: place_lat, place_lon: place_lon }}">周辺レストラン</router-link>
    <br />
    
    <h2>Like登録</h2>
    <br />
    <div v-if="like_exist == 'yes'">
    registered \(^o^
    </div>
    <div v-else>
        <button @click="likeregister">Like</button>
    </div>
    <br />
  </div>
</template>

<script>
import firebase from 'firebase'
import 'firebase/firestore'

export default {
  name: 'Place',
  data () {
    return {
      place_name: '',
      place_address: '',
      place_wiki: '',
      place_imageurl: '',
      place_lat: '',
      place_lon: '',
      user_id: '',
      visitday: '',
      postvisit:'',
      like_exist: 'no',
      place_id: ''
    }
  },
  created: function(){
    const params = this.$route.params.place_id
    console.log(params)
    firebase.firestore().collection('place').doc(params).get().then((doc) => {
        console.log(doc.data())
        this.place_name = doc.data().name
        this.place_address = doc.data().address
        this.place_wiki = doc.data().detail
        this.place_imageurl = doc.data().image_url
        this.place_lat = doc.data().latitude
        this.place_lon = doc.data().longitude
        this.place_id = doc.id
    })
    const uid = firebase.auth().currentUser.uid
    console.log(uid)
    firebase.firestore().collection('like').where('user_email','==',uid).where('place_id','==',params).get().then((querySnapshot) => {
    querySnapshot.forEach((doc) => {
        this.like_exist = 'yes';
        console.log(this.like_exist)
        })
    })
  },
    methods: {
        likeregister: function () {
            firebase.firestore().collection('like').add({
                place_id: this.place_id,
                place_imageurl: this.place_imageurl,
                place_name: this.place_name,
                user_email: this.user_id
            }).catch(function (error) {
                console.error('Error adding document: ', error);
            })
                console.log('登録');
                this.like_exist =='yes'
        }
    }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  margin: 0 10px;
}
a {
  color: #42b983;
}
input {
  margin: 10px 0;
  padding: 10px;
}
h2 {
    background:#1c4b7a;
    color:#fff;
}
</style>

routerの個所に遷移先で必要となるパラメータを持たせています。極力Firebaseとの接続を行わず、一度取得したデータはうまく使いまわすような形で実装できると理想ですね。ただ、コードがごちゃごちゃしてくるので、トレードオフなところもあります。。。

これと同じ要領で、jalan(お宿のほう)のAPIを使って画面を作ってみてくださいね。え、axios使ってないって?そう、axiosはjalanさんのほうで使います。。。yelpでは不要でございました。

あと、さいごに検索機能をAlgoliaでちょちょいと付け足しておきましょう。
https://qiita.com/moksahero/items/ccb156e1975d79ffac2d
辺りも参考にどうぞ。

検索の対象はPlaceです。なので、spot.jsonがいい感じ。けど、リアルタイムに出てくる検索結果にリンクも付けたいんで、コレクションのドキュメントIDをうまくくっつけてあげてください。下の場合はplaceid(ドキュメントID)をAlgoliaに流しています。

views/Search.vue
<template>
<div>
  <ais-index
    app-id="XXXXyyyy"
    api-key="XXXXyyyyyzzzzz"
    index-name="place"
  >
  <br><br>
    <ais-input placeholder="キーワードを入力"></ais-input>
    <ais-results>
      <template slot-scope="{ result }">
        <a :href="'#/place/' + result.placeid">
        <p><strong>{{ result.name }}</strong>{{ result.address }}
        </p>
      </a>
      </template>
    </ais-results>
  </ais-index>
</div>
</template>

<script>
export default {
  name: 'search',
  data () {
    return {
    }
  },
  created: function(){
  },
  methods: {
  }
}
</script>

<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  margin: 0 10px;
}
a {
  color: #42b983;
}
input {
  margin: 10px 0;
  padding: 10px;
}
button.ais-search-box__submit{
    display:none;
}
button.ais-clear {
    display:none;
}
p {
  text-align:left;
  margin-left:20px;
  margin-bottom:10px;
  font-size:16px;
}
</style>

これで骨格は出来上がりました。あとは応用しながらアプリケーションを作りこんでいきましょう。よきVueライフを~

\(^o^)/オワタ

15
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?