Compare commits

...

4 Commits

Author SHA1 Message Date
8cb4a14b94
feat: login & logout flow in Navbar 2023-06-10 02:46:29 +08:00
0e2a08daf3
feat: add cookie after registering
import vue-cookie to log whether the user logined.
2023-06-09 22:56:44 +08:00
f907dd5adb
feat: client main page 2023-06-09 22:37:00 +08:00
4ea494cdff
feat: 'add warning table in credit page'
but haven't implement query
2023-06-09 02:22:25 +08:00
11 changed files with 579 additions and 214 deletions

22
package-lock.json generated
View File

@ -9,8 +9,10 @@
"version": "0.0.0",
"dependencies": {
"@metamask/detect-provider": "^2.0.0",
"html5-qrcode": "^2.3.8",
"pinia": "^2.0.36",
"vue": "^3.3.2",
"vue-cookies": "^1.8.3",
"vue-router": "^4.2.0",
"web3": "^1.8.2"
},
@ -3350,6 +3352,11 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/html5-qrcode": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz",
"integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ=="
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@ -5825,6 +5832,11 @@
"@vue/shared": "3.3.4"
}
},
"node_modules/vue-cookies": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/vue-cookies/-/vue-cookies-1.8.3.tgz",
"integrity": "sha512-VBRsyRMVdahBgFfh389TMHPmDdr4URDJNMk4FKSCfuNITs7+jitBDhwyL4RJd3WUsfOYNNjPAkfbehyH9AFuoA=="
},
"node_modules/vue-eslint-parser": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.0.tgz",
@ -8823,6 +8835,11 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
"html5-qrcode": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz",
"integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ=="
},
"http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@ -10593,6 +10610,11 @@
"@vue/shared": "3.3.4"
}
},
"vue-cookies": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/vue-cookies/-/vue-cookies-1.8.3.tgz",
"integrity": "sha512-VBRsyRMVdahBgFfh389TMHPmDdr4URDJNMk4FKSCfuNITs7+jitBDhwyL4RJd3WUsfOYNNjPAkfbehyH9AFuoA=="
},
"vue-eslint-parser": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.0.tgz",

View File

@ -9,8 +9,10 @@
},
"dependencies": {
"@metamask/detect-provider": "^2.0.0",
"html5-qrcode": "^2.3.8",
"pinia": "^2.0.36",
"vue": "^3.3.2",
"vue-cookies": "^1.8.3",
"vue-router": "^4.2.0",
"web3": "^1.8.2"
},

View File

@ -1,219 +1,239 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "SBT_addr",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
"inputs": [
{
"internalType": "address",
"name": "SBT_addr",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "recv",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
"inputs": [],
"name": "recv",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "sbt",
"outputs": [
{
"internalType": "contract SoulboundToken",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
"inputs": [],
"name": "sbt",
"outputs": [
{
"internalType": "contract SoulboundToken",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"stateMutability": "payable",
"type": "receive",
"payable": true
"stateMutability": "payable",
"type": "receive",
"payable": true
},
{
"inputs": [
"inputs": [
{
"internalType": "address",
"name": "client",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "setCredit",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "number",
"type": "uint256"
}
],
"name": "register",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "shop",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "pay",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "repay",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function",
"payable": true
},
{
"inputs": [],
"name": "start_recv",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "stop_recv",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "client",
"type": "address"
}
],
"name": "getCredit",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "client",
"type": "address"
}
],
"name": "getArrear",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "client",
"type": "address"
}
],
"name": "getClientOrders",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "client",
"type": "address"
"internalType": "bool",
"name": "isFinished",
"type": "bool"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "setCredit",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "number",
"type": "uint256"
}
],
"name": "register",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "shop",
"type": "address"
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
"internalType": "address",
"name": "shop",
"type": "address"
}
],
"name": "pay",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
],
"internalType": "struct Order[]",
"name": "",
"type": "tuple[]"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "repay",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function",
"payable": true
},
{
"inputs": [],
"name": "start_recv",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "stop_recv",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "client",
"type": "address"
}
],
"name": "getCredit",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "client",
"type": "address"
}
],
"name": "getArrear",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "client",
"type": "address"
}
],
"name": "getClientOrders",
"outputs": [
{
"components": [
{
"internalType": "bool",
"name": "isFinished",
"type": "bool"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "address",
"name": "shop",
"type": "address"
}
],
"internalType": "struct Order[]",
"name": "",
"type": "tuple[]"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
"inputs": [
{
"internalType": "address",
"name": "client",
"type": "address"
}
],
"name": "getSBTNumber",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
}
]
]

View File

@ -39,6 +39,18 @@ hr {
background: #060606;
color: white;
} */
.panel {
background-color: #F0F0F0;
}
.panel-heading {
background-color: #5B5B5B;
color: white;
}
.panel-block {
color: black;
}
.box.is-fullwidth h1.title.is-4{
color: #060606;

View File

@ -0,0 +1,74 @@
<script>
export default {
name: 'ClientNav',
props: ['path'],
mounted () {
console.log(this.path)
this.navCSS[this.path] += ' is-active'
console.log(this.navCSS)
},
data () {
return {
navCSS: {
main: 'panel-block',
pay: 'panel-block',
credit: 'panel-block',
info: 'panel-block',
}
}
}
}
</script>
<template>
<nav class="panel">
<p class="panel-heading">
使用者選項
</p>
<RouterLink to="/client" :class="this.navCSS['main']">
<span class="panel-icon">
<i class="fas fa-book" aria-hidden="true"></i>
</span>
<template v-if="this.path == 'main'">
<strong>主選單</strong>
</template>
<template v-else>
<p>主選單</p>
</template>
</RouterLink>
<RouterLink to="/client/info" :class="this.navCSS['info']">
<span class="panel-icon">
<i class="fas fa-book" aria-hidden="true"></i>
</span>
<template v-if="this.path == 'info'">
<strong>個人資料</strong>
</template>
<template v-else>
<p>個人資料</p>
</template>
</RouterLink>
<RouterLink to="/client/pay" :class="this.navCSS['pay']">
<span class="panel-icon">
<i class="fas fa-book" aria-hidden="true"></i>
</span>
<template v-if="this.path == 'pay'">
<strong>掃描支付</strong>
</template>
<template v-else>
<p>掃描支付</p>
</template>
</RouterLink>
<RouterLink to="/client/credit" :class="this.navCSS['credit']">
<span class="panel-icon">
<i class="fas fa-book" aria-hidden="true"></i>
</span>
<template v-if="this.path == 'credit'">
<strong>信用紀錄</strong>
</template>
<template v-else>
<p>信用紀錄</p>
</template>
</RouterLink>
</nav>
</template>

View File

@ -1,6 +1,79 @@
<script>
import detectEthereumProvider from '@metamask/detect-provider'
import WarningModal from '../components/WarningModal.vue'
import SuccessModal from '../components/SuccessModal.vue'
import Web3 from 'web3'
import Bank from '@/assets/Bank.json'
export default {
name: 'PageNavbar'
components: { WarningModal, SuccessModal },
name: 'PageNavbar',
data() {
return {
BankAddress: import.meta.env.VITE_BANK_ADDR,
linked: false,
address: '',
msg:'',
successModalStatus: false,
warningModalStatus: false,
web3: null,
}
},
mounted() {
console.log(this.$cookies.isKey('linked'))
if (!this.$cookies.isKey('linked')) {
this.linked = false
} else {
this.linked = true
this.address = this.$cookies.get('address')
}
},
methods: {
async login() {
const provider = await detectEthereumProvider()
if (provider) {
const chainId = await window.ethereum.request({ method: 'eth_chainId' })
console.log(chainId)
if (chainId == import.meta.env.VITE_CHAIN_ID) {
const account = await window.ethereum.request({ method: 'eth_requestAccounts' })
// check from bank
this.web3 = new Web3(window.ethereum)
var token = new this.web3.eth.Contract(Bank, this.BankAddress)
var clientAddr = (await this.web3.eth.getAccounts())[0]
this.web3.eth.defaultAccount = clientAddr
var returnNumber = await token.methods.getSBTNumber(clientAddr).call({from: clientAddr})
if (returnNumber != 0){
this.$cookies.set('address', clientAddr)
this.$cookies.set('linked', true)
this.$cookies.set('SBTNumber', returnNumber)
this.linked = true
this.address = this.$cookies.get('address')
this.msg = '成功連接 MetaMask'
this.successModalStatus = true
} else {
this.msg = '您可能還沒有註冊過!'
this.warningModalStatus = true
}
} else {
this.msg = '你連接的不是 Sepolia 測試網路,目前只接受 Sepolia address'
this.warningModalStatus = true
}
} else {
this.msg = 'no Metamask'
this.warningModalStatus = true
}
},
logout () {
console.log('logout')
this.linked = false
this.address = ''
this.$cookies.remove('linked')
this.$cookies.remove('address')
this.$cookies.remove('SBTNumber')
this.$router.push('/')
}
}
}
</script>
@ -31,10 +104,16 @@ export default {
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<RouterLink to="/signup" class="button is-primary is-outlined">Sign up</RouterLink>
<RouterLink to="/login" class="button is-info is-outlined">Log in</RouterLink>
</div>
<template v-if="!linked">
<div class="buttons">
<RouterLink to="/signup" class="button is-primary is-outlined">Sign up</RouterLink>
<a class="button is-info is-outlined" @click="login">Log in</a>
</div>
</template>
<template v-else>
<RouterLink to="/client" class="button is-info is-outlined is-small is-rounded">{{ 'Hello! ' + address }}</RouterLink>
<button class="button is-danger is-outlined is-small is-rounded" @click="logout">登出</button>
</template>
</div>
</div>
</div>
@ -42,4 +121,7 @@ export default {
</div>
</div>
</section>
<WarningModal :active="warningModalStatus" :errorMsg="msg" @closeModal="warningModalStatus=false"></WarningModal>
<SuccessModal :active="successModalStatus" :successMsg="msg" @closeModal="successModalStatus=false" link="/client" btnName="繼續"></SuccessModal>
</template>

View File

@ -3,10 +3,12 @@ import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import VueCookies from 'vue-cookies'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(VueCookies, { expires: '7d'})
app.mount('#app')

View File

@ -3,6 +3,10 @@ import HomeView from '../views/HomeView.vue'
import SignupView from '../views/SignupView.vue'
import LinkSBTView from '../views/LinkSBTView.vue'
import CreditView from '../views/CreditView.vue'
import ClientMainView from '../views/ClientMainView.vue'
import ClientInfoView from '../views/ClientInfoView.vue'
import ClientCreditView from '../views/ClientCreditView.vue'
import ClientPayView from '../views/ClientPayView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -26,7 +30,27 @@ const router = createRouter({
path: '/signup/credit',
name: 'startcredit',
component: CreditView
}
},
{
path: '/client',
name: 'clientmain',
component: ClientMainView
},
{
path: '/client/info',
name: 'clientinfo',
component: ClientInfoView
},
{
path: '/client/credit',
name: 'clientcredit',
component: ClientCreditView
},
{
path: '/client/pay',
name: 'clientpay',
component: ClientPayView
},
]
})

View File

@ -0,0 +1,111 @@
<script>
import Web3 from 'web3';
import SBT from '@/assets/SBT.json'
import PageTitle from '../components/PageTitle.vue'
import detectEthereumProvider from '@metamask/detect-provider'
import WarningModal from '../components/WarningModal.vue'
import SuccessModal from '../components/SuccessModal.vue'
import { useClientStore } from '../stores/Client.js'
import ClientNav from '../components/ClientNav.vue'
// To use Html5QrcodeScanner (more info below)
import { Html5QrcodeScanner } from "html5-qrcode";
// To use Html5Qrcode (more info below)
import { Html5Qrcode } from "html5-qrcode";
export default {
components: { PageTitle, WarningModal, SuccessModal, ClientNav },
data() {
return {
SBTAddress: import.meta.env.VITE_SBT_ADDR,
number: 0,
warningModalStatus: false,
successModalStatus: false,
msg: '',
clientAddr: '',
web3: null,
token: null,
isWaiting: false,
log: [],
scanner: null
}
},
async mounted() {
this.web3 = new Web3(window.ethereum)
this.clientAddr = (await this.web3.eth.getAccounts())[0]
this.web3.eth.defaultAccount = this.clientAddr
},
methods: {
onScanSuccess(decodedText, decodedResult) {
// handle the scanned code as you like, for example:
console.log(`Code matched = ${decodedText}`, decodedResult);
this.scanner.clear()
},
scan () {
this.scanner = new Html5QrcodeScanner(
"reader",
{ fps: 10, qrbox: { width: 250, height: 250 } },
/* verbose= */ false);
this.scanner.render(this.onScanSuccess);
}
}
}
</script>
<template>
<section class="blog-posts">
<div class="container">
<div class="columns">
<div class="column is-2">
<!-- <ClientNav path="main"></ClientNav> -->
</div>
<div class="column">
<div class="container">
<div class="block">
<PageTitle title="Set Credit Limit" subtitle="根據 SBT 信用紀錄設定額度"></PageTitle>
</div>
<div class="block">
<div class="tile is-ancestor">
<div class="tile is-vertical is-8">
<div class="tile">
<div class="tile is-parent is-vertical">
<RouterLink to="/client/info" class="tile is-child notification is-info">
<article>
<p class="title"><i class="fas fa-user"></i> 個人資料</p>
<!-- <p class="subtitle">Top tile</p> -->
</article>
</RouterLink>
<RouterLink to="/client/pay" class="tile is-child notification is-info">
<article>
<p class="title"><i class="fas fa-credit-card"></i> 掃描支付</p>
<!-- <p class="subtitle">Top tile</p> -->
</article>
</RouterLink>
<RouterLink to="/client/credit" class="tile is-child notification is-info">
<article>
<p class="title"><i class="fas fa-history"></i> 信用紀錄</p>
<!-- <p class="subtitle">Top tile</p> -->
</article>
</RouterLink>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<WarningModal :active="warningModalStatus" :errorMsg="msg" @closeModal="warningModalStatus = false"></WarningModal>
<SuccessModal :active="successModalStatus" :successMsg="msg" @closeModal="successModalStatus = false"
link="/signup/linksbt" btnName="繼續"></SuccessModal>
</template>

View File

@ -109,23 +109,35 @@ export default {
</template>
</tbody>
</table>
</div>
<div class="block">
<h1 class="title is-4">警告紀錄</h1>
<table class="table is-fullwidth is-striped is-hoverable">
<thead>
<tr>
<th>#</th>
<th>發起預警之銀行</th>
</tr>
</thead>
<tbody>
<template v-for="(value, index) of log">
<tr>
<th>{{ index }}</th>
<td>{{ value.bank }}</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="block">
<div class="columns">
<div class="column is-2 is-offset-4">
<button @click="detect" class="button is-primary is-fullwidth is-medium is-outlined">Link to
MetaMask</button>
</div>
<div class="column is-2">
<RouterLink to="/" class="button is-danger is-fullwidth is-medium is-outlined">Cancel</RouterLink>
<div class="column is-2 is-offset-5">
<RouterLink to="/client" class="button is-primary is-fullwidth is-medium is-outlined">確認</RouterLink>
</div>
</div>
</div>
</div>
</section>
<WarningModal :active="warningModalStatus" :errorMsg="msg" @closeModal="warningModalStatus = false"></WarningModal>
<SuccessModal :active="successModalStatus" :successMsg="msg" @closeModal="successModalStatus = false"
link="/signup/linksbt" btnName="繼續"></SuccessModal>
</template>

View File

@ -42,6 +42,10 @@ export default {
try {
await this.bank.methods.register(this.number).send({ from: this.clientAddr })
this.successModalStatus = true
this.$cookies.set('linked', true)
this.$cookies.set('SBTNumber', returnNumber)
this.$cookies.set('address', this.clientAddr)
this.link = '/signup/credit'
this.msg = '註冊成功!'
} catch (error) {