Просмотр исходного кода

Show billing prompt if there are payment errors

automatically prompts for a new card if past payments were
unsuccessful. Does not actually disable API access.
master
Immanuel Onyeka 4 месяцев назад
Родитель
Сommit
1199bc6d08
9 измененных файлов: 238 добавлений и 14 удалений
  1. +33
    -1
      components/app.vue
  2. +1
    -1
      components/dialog.vue
  3. +2
    -5
      components/login.vue
  4. +42
    -0
      components/update-billing/billing.vue
  5. +19
    -0
      components/update-billing/completed.vue
  6. +35
    -0
      components/update-billing/prompt.vue
  7. +86
    -0
      components/update-billing/update-billing.vue
  8. +7
    -5
      main.css
  9. +13
    -2
      skouter.go

+ 33
- 1
components/app.vue Просмотреть файл

@@ -2,6 +2,7 @@
<div class="app-panel">

<template v-if="user">

<side-bar v-if="user"
:role="user && user.status"
:avatar="user.avatar"
@@ -13,6 +14,7 @@
<spinner></spinner>
</div>


<home :user="user" v-else-if="active == 1" />
<new-estimate
:user="user"
@@ -37,6 +39,8 @@ v-else-if="active == 3"
@updateLetterhead="updateLetterhead"
v-else-if="active == 4" />

<biller v-if="invalidSub" :user="user"/>

<sign-out :user="user" v-else-if="active == 5" />

</template>
@@ -57,6 +61,7 @@ import Estimates from "./estimates.vue"
import Settings from "./settings.vue"
import SignOut from "./sign-out.vue"
import Login from "./login.vue"
import Biller from "./update-billing/update-billing.vue"

function getCookie(name) {
var re = new RegExp(name + "=([^;]+)")
@@ -86,6 +91,20 @@ function refreshToken() {
})
}

function fetchUser() {
return fetch(`/api/user`,
{method: 'GET',
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${getCookie("skouter")}`,
},
}).then(response => {
if (response.ok) {
return response.json()
}
})
}

function getUser() {
const token = getCookie("skouter")
this.token = token
@@ -206,17 +225,28 @@ function active() {
// Fetch data before showing UI. If requests fail, assume token is expired.
function start() {
this.loading = true
const validStatuses = ["incomplete", "trialing", "active"]

let loaders = []
loaders.push(this.getUser())
loaders.push(this.getFees())
Promise.all(loaders).then((a, b) => {
this.loading = false
if (!b) {
if (!b) { // !b means there is no rejection error for any promise
// Time untill token expiration may have elapsed before the page
// reloaded
this.refreshToken()
fetchUser().then( u => {
if (u.sub.customerId &&
validStatuses.includes(u.sub.paymentStatus)) {
return
}
// Payment must be updated
console.log("paying...")
this.invalidSub = true
})
}
window.location.hash = ''
}).catch(error => {
console.log("An error occured %O", error)
this.loadingError = "Could not initialize app."
@@ -249,6 +279,7 @@ export default {
NewEstimate,
Estimates,
Settings,
Biller,
SignOut,
Login
},
@@ -273,6 +304,7 @@ export default {
fees: [],
loadingError: "",
token: '',
invalidSub: false,
}
},
created() {


+ 1
- 1
components/dialog.vue Просмотреть файл

@@ -1,5 +1,5 @@
<template>
<div class="modal">
<div class="modal-prompt">

<section class="form inputs dialog">
<img width="21" height="21" src="/assets/image/icon/x-red.svg"


+ 2
- 5
components/login.vue Просмотреть файл

@@ -23,14 +23,11 @@ function login() {
body: JSON.stringify( {email: this.email, password: this.password} ),
}).then(response => {
if (response.ok) {
return response.text()
this.$emit('login')
window.location.hash = ''
} else {
this.error = "Invalid credentials"
}
}).then(result => {
if (!result || !result.length) return // Exit if there is no token
this.$emit('login')
window.location.hash = ''
})

}


+ 42
- 0
components/update-billing/billing.vue Просмотреть файл

@@ -0,0 +1,42 @@
<template>
<div>

<h4>Billing</h4>
<div id="payment-element"></div>
<div id="message"></div>
<button @click="submit" class="btn btn-primary">Submit</button>

</div>
</template>

<script setup>
import { ref, onMounted } from "vue"

const props = defineProps(["sub"])
const stripe = Stripe(process.env.STRIPE_KEY)
const options = { clientSecret: props.sub.clientSecret }
const elements = stripe.elements(options)
const payEl = elements.create("payment")

function submit() {
let result = stripe.confirmPayment({
//`Elements` instance that was used to create the Payment Element
elements,
confirmParams: {
return_url: "https://skouter.net/register",
}
})
}

onMounted(() => {
payEl.mount("#payment-element")
})
</script>

<style scoped>
button.btn {
margin: 10px auto;
display: block;
min-width: 90px;
}
</style>

+ 19
- 0
components/update-billing/completed.vue Просмотреть файл

@@ -0,0 +1,19 @@
<template>
<section>
<h4 v-if="status == 'succeeded'">Payment succeeded!</h4>
<h4 v-if="status == 'processing'">Payment still processing. Try again later.</h4>
</section>
</template>

<script setup>
import { ref, onMounted } from "vue"

const props = defineProps(["status"])

</script>

<style>
h4 {
text-align: center;
}
</style>

+ 35
- 0
components/update-billing/prompt.vue Просмотреть файл

@@ -0,0 +1,35 @@
<template>
<section>
<p>
Your payment information is missing or no longer valid, please update your billing details.
</p>
<button @click="() => $emit('ok')">OK</button>
</section>
</template>

<script setup>
import { ref } from "vue"

const emits = defineEmits(['ok'])
</script>

<style scoped>
form > div {
display: flex;
justify-content: space-between;
margin: 10px;
position: relative;
}

button {
margin: auto;
min-width: 90px;
display: block;
}

span.error {
margin: 10px auto;
color: darkred;
}
</style>

+ 86
- 0
components/update-billing/update-billing.vue Просмотреть файл

@@ -0,0 +1,86 @@
<template>
<div class="modal-prompt">

<section class="shadowbox">
<h2>Update Billing</h2>
<prompt v-if="step == 1" :err="err" @ok="() => step++" />
<billing v-if="step == 2" :err="err" :sub="user.sub"/>
<completed v-if="step == 3" :err="err" :status="sub.paymentStatus"/>
</section>

</div>
</template>

<script setup>
import { ref, onMounted } from "vue"
import Dialog from "../dialog.vue"
import Prompt from "./prompt.vue"
import Billing from "./billing.vue"
import Completed from "./completed.vue"

let err = ref("")
const props = defineProps(['user'])
const stripe = Stripe(process.env.STRIPE_KEY)
const step = ref(1)
const token = ref("")

const sub = ref(null)

const clientSecret = new URLSearchParams(window.location.search).get(
'payment_intent_client_secret'
);

function getCookie(name) {
var re = new RegExp(name + "=([^;]+)")
var value = re.exec(document.cookie)

return (value != null) ? unescape(value[1]) : null
}

function intent(u) {
return
return fetch(`/api/user/subscribe`,
{method: 'POST',
body: JSON.stringify(u),
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${token.value}`,
},
}).then(resp => {
if (resp.ok) {
resp.json().then(s => {
err.value = ""
console.log(s)
sub.value = s
if (["processing", "succeeded"].includes(s.paymentStatus) &&
clientSecret == s.clientSecret) {
step.value = step.value + 2
} else if (s.paymentStatus == "requires_payment_method") {
step.value++
} else {
step.value = 0
}
})
} else {
resp.text().then( e => err.value = e)
}
})
}

onMounted(() => {
console.log(props.user)
})

</script>

<style scoped>
section {
max-width: 400px;
margin: auto;
}

div.modal-prompt .form {
}
</style>

+ 7
- 5
main.css Просмотреть файл

@@ -342,29 +342,31 @@ section.form .fee img {
margin-left: auto;
}

div.modal {
div.modal-prompt {
position: fixed;
z-index: 5;
width: 100vw;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
display: block;
opacity: 1;
padding: 10px;
}

div.modal .form {
div.modal-prompt .form {
z-index: 5;
margin: auto auto;
margin-top: 10%;
width: 100%;
max-width: 300px;
max-width: 90%;
padding: 20px 10px;
background: white;
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
border-radius: 3px;
}

div.modal .form img.close-btn {
div.modal-prompt .form img.close-btn {
right: 10px;
top: 10px;
position: absolute;


+ 13
- 2
skouter.go Просмотреть файл

@@ -1191,8 +1191,12 @@ func queryUser(db *sql.DB, id int) (User, error) {
u.verified,
u.role,
u.address,
u.phone
FROM user u WHERE u.id = ?
u.phone,
s.id
FROM user u
LEFT JOIN subscription s
ON s.user_id = u.id
WHERE u.id = ?
`
row := db.QueryRow(query, id)

@@ -1214,6 +1218,7 @@ func queryUser(db *sql.DB, id int) (User, error) {
&user.Role,
&user.Address.Id,
&user.Phone,
&user.Sub.Id,
)

if err != nil {
@@ -1224,6 +1229,11 @@ func queryUser(db *sql.DB, id int) (User, error) {
if err != nil {
return user, err
}
user.Sub, err = querySub(db, user.Sub.Id)
if err != nil {
return user, err
}

if user.Branch.Id > 0 {
user.Branch, err = queryBranch(db, user.Branch.Id)
@@ -1387,6 +1397,7 @@ func querySub(db *sql.DB, id int) (Subscription, error) {
err = row.Scan(
&s.Id,
&s.StripeId,
&s.UserId,
&s.CustomerId,
&s.End,
&s.Start,


Загрузка…
Отмена
Сохранить