Compare commits

...

3 Commits

Author SHA1 Message Date
fde0fdb7fa Working on js for plans 2024-08-11 22:58:06 +02:00
85c1928c31 move some common parts in common.js 2024-08-11 22:51:34 +02:00
16e5845559 Creating front end page for plans 2024-08-11 22:08:55 +02:00
13 changed files with 289 additions and 112 deletions

View File

@ -260,3 +260,20 @@ func (app *application) ChargeOnce(w http.ResponseWriter, r *http.Request) {
app.errorLog.Println(err) app.errorLog.Println(err)
} }
} }
func (app *application) BronzePlan(w http.ResponseWriter, r *http.Request) {
widget, err := app.DB.GetWidget(2)
if err != nil {
app.errorLog.Println(err)
return
}
data := make(map[string]interface{})
data["widget"] = widget
if err := app.renderTemplate(w, r, "bronze-plan", &templateData{
Data: data,
}); err != nil {
app.errorLog.Println(err)
}
}

View File

@ -19,6 +19,8 @@ func (app *application) routes() http.Handler {
mux.Get("/receipt", app.Receipt) mux.Get("/receipt", app.Receipt)
mux.Post("/payment-succeeded", app.PaymentSucceeded) mux.Post("/payment-succeeded", app.PaymentSucceeded)
mux.Get("/plans/bronze", app.BronzePlan)
fileServer := http.FileServer(http.Dir("./static")) fileServer := http.FileServer(http.Dir("./static"))
mux.Handle("/static/*", http.StripPrefix("/static", fileServer)) mux.Handle("/static/*", http.StripPrefix("/static", fileServer))

View File

@ -46,7 +46,7 @@
<a class="dropdown-item" href="/widget/1">Buy one widget</a> <a class="dropdown-item" href="/widget/1">Buy one widget</a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="#">Subscription</a> <a class="dropdown-item" href="/plans/bronze">Subscription</a>
</li> </li>
</ul> </ul>
</li> </li>

View File

@ -0,0 +1,102 @@
{{ template "base" . }}
{{ define "title" }}
Bronze Plan
{{ end }}
{{ define "content" }}
{{$widget := index .Data "widget"}}
<h2 class="mt-3 text-center">Bronze Plan: {{formatCurrency $widget.Price}}</h2>
<hr>
<div class="alert alert-danger text-center d-none" id="card-messages"></div>
<form action="/payment-succeeded-temp"
method="post"
name="charge_form"
id="charge_form"
class="d-blick needs-validation charge-form"
autocomplete="off"
novalidate="">
<input type="hidden" name="product_id" value="{{$widget.ID}}">
<input type="hidden" id="amount" name="amount" value="{{$widget.Price}}">
<p class="mt-2 mb-3 text-center">{{$widget.Description}}</p>
<hr>
<div class="mb-3">
<label for="first-name" class="form-label">First Name</label>
<input type="text"
id="first-name"
name="first_name"
autocomplete="first-name-new"
required=""
class="form-control">
</div>
<div class="mb-3">
<label for="last-name" class="form-label">Last Name</label>
<input type="text"
id="last-name"
name="last_name"
autocomplete="last-name-new"
required=""
class="form-control">
</div>
<div class="mb-3">
<label for="cardholder-email" class="form-label">Email</label>
<input type="text"
id="cardholder-email"
name="cardholder_email"
autocomplete="cardholder-email-new"
required=""
class="form-control">
<div class="mb-3">
<label for="cardholder-name" class="form-label">Name on card</label>
<input type="text"
id="cardholder-name"
name="cardholder_name"
autocomplete="cardholder-name-new"
required=""
class="form-control">
</div>
</div>
<!-- card number will be built by stripe -->
<div class="mb-3">
<label for="card-element" class="form-label">Credit Card</label>
<div class="form-control" id="card-element"></div>
<div class="alert-danger text-center" id="card-errors" role="alert"></div>
<div class="alert-success text-center" id="card-success" role="alert"></div>
</div>
<hr>
<a href="javascript:void(0)"
id="pay-button"
class="btn btn-primary"
onclick="val({{.API}})">Pay {{formatCurrency $widget.Price}}/month</a>
<div class="text-center d-none" id="processing-payment">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<input id="payment_intent"
type="hidden"
name="payment_intent"
value="payment_intent">
<input id="payment_method"
type="hidden"
name="payment_method"
value="payment_method">
<input id="payment_amount"
type="hidden"
name="payment_amount"
value="payment_amount">
<input id="payment_currency"
type="hidden"
name="payment_currency"
value="payment_currency">
</form>
{{ end }}
{{ define "js" }}
<script src="https://js.stripe.com/v3/"></script>
<script type="module">
import {stripeInit} from "/static/js/common.js";
import {val} from "/static/js/stripe-plan.js"
stripeInit('{{.StripePubKey}}');
document.getElementById("pay-button").addEventListener("click", () => {
val({{.API}});
})
</script>
{{ end }}

View File

@ -66,10 +66,7 @@ Buy one widget
<div class="alert-success text-center" id="card-success" role="alert"></div> <div class="alert-success text-center" id="card-success" role="alert"></div>
</div> </div>
<hr> <hr>
<a href="javascript:void(0)" <a href="javascript:void(0)" id="pay-button" class="btn btn-primary">Charge Card</a>
id="pay-button"
class="btn btn-primary"
onclick="val({{.API}})">Charge Card</a>
<div class="text-center d-none" id="processing-payment"> <div class="text-center d-none" id="processing-payment">
<div class="spinner-border text-primary" role="status"> <div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
@ -95,6 +92,12 @@ Buy one widget
{{ end }} {{ end }}
{{ define "js" }} {{ define "js" }}
<script src="https://js.stripe.com/v3/"></script> <script src="https://js.stripe.com/v3/"></script>
<script src="/static/js/stripe.js"></script> <script type="module">
<script>stripeInit('{{.StripePubKey}}');</script> import {stripeInit} from "/static/js/common.js";
import {val} from "/static/js/stripe.js"
stripeInit('{{.StripePubKey}}');
document.getElementById("pay-button").addEventListener("click", () => {
val({{.API}});
})
</script>
{{ end }} {{ end }}

View File

@ -49,10 +49,7 @@ Virtual Terminal
<div class="alert-success text-center" id="card-success" role="alert"></div> <div class="alert-success text-center" id="card-success" role="alert"></div>
</div> </div>
<hr> <hr>
<a href="javascript:void(0)" <a href="javascript:void(0)" id="pay-button" class="btn btn-primary">Charge Card</a>
id="pay-button"
class="btn btn-primary"
onclick="val({{.API}})">Charge Card</a>
<div class="text-center d-none" id="processing-payment"> <div class="text-center d-none" id="processing-payment">
<div class="spinner-border text-primary" role="status"> <div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
@ -78,8 +75,9 @@ Virtual Terminal
{{ end }} {{ end }}
{{ define "js" }} {{ define "js" }}
<script src="https://js.stripe.com/v3/"></script> <script src="https://js.stripe.com/v3/"></script>
<script src="/static/js/stripe.js"></script> <script type="module">
<script> import {stripeInit} from "/static/js/common.js";
import {val} from "/static/js/stripe.js"
stripeInit('{{.StripePubKey}}'); stripeInit('{{.StripePubKey}}');
document.getElementById("charge_amount").addEventListener("change", (evt) => { document.getElementById("charge_amount").addEventListener("change", (evt) => {
if (evt.target.value !== "") { if (evt.target.value !== "") {
@ -88,13 +86,10 @@ Virtual Terminal
document.getElementById("amount").value = 0; document.getElementById("amount").value = 0;
} }
}); });
// function termVal() {
// const chargeAmount = document.getElementById("charge_amount"); document.getElementById("pay-button").addEventListener("click", () => {
// if (chargeAmount.value !== "") { val({{.API}});
// document.getElementById("amount").value = parseInt((chargeAmount.value * 100), 10); })
// } else {
// document.getElementById("amount").value = 0;
// }
// }
</script> </script>
{{ end }} {{ end }}

View File

@ -15,4 +15,3 @@ Payment Succedded!
<p>Bank Return Code: {{ $txn.BankReturnCode }}</p> <p>Bank Return Code: {{ $txn.BankReturnCode }}</p>
<p>Expiry Date: {{ $txn.ExpiryMonth }}/{{$txn.ExpiryYear}}</p> <p>Expiry Date: {{ $txn.ExpiryMonth }}/{{$txn.ExpiryYear}}</p>
{{ end }} {{ end }}

View File

@ -33,6 +33,8 @@ type Widget struct {
CreatedAt time.Time `json:"-"` CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"`
Image string `json:"image"` Image string `json:"image"`
IsRecurring bool `json:"is_recurring"`
PlanID string `json:"plan_id"`
} }
// Order is the type for all orders // Order is the type for all orders
@ -109,6 +111,7 @@ func (m *DBModel) GetWidget(id int) (Widget, error) {
query := `SELECT id, name, description, inventory_level, price, query := `SELECT id, name, description, inventory_level, price,
coalesce(image, ''), coalesce(image, ''),
is_recurring, plan_id,
created_at, updated_at created_at, updated_at
FROM widgets WHERE id = ?;` FROM widgets WHERE id = ?;`
@ -120,6 +123,8 @@ func (m *DBModel) GetWidget(id int) (Widget, error) {
&widget.InventoryLevel, &widget.InventoryLevel,
&widget.Price, &widget.Price,
&widget.Image, &widget.Image,
&widget.IsRecurring,
&widget.PlanID,
&widget.CreatedAt, &widget.CreatedAt,
&widget.UpdatedAt, &widget.UpdatedAt,
) )

View File

@ -0,0 +1,2 @@
drop_column("widgets", "is_recurring")
drop_column("widgets", "plan_id")

View File

@ -0,0 +1,2 @@
add_column("widgets", "is_recurring", "bool", {"default": 0})
add_column("widgets", "plan_id", "string", {"default": ""})

63
static/js/common.js Normal file
View File

@ -0,0 +1,63 @@
export let card;
export let stripe;
const cardMessages = document.getElementById("card-messages");
const payButton = document.getElementById("pay-button");
export const processing = document.getElementById("processing-payment");
export function hidePayButton() {
payButton.classList.add("d-none");
processing.classList.remove("d-none");
}
export function showPayButton() {
payButton.classList.remove("d-none");
processing.classList.add("d-none");
}
export function showCardError(msg) {
cardMessages.classList.add("alert-danger");
cardMessages.classList.remove("alert-success");
cardMessages.classList.remove("d-none");
cardMessages.innerText = msg;
}
export function showCardSuccess() {
cardMessages.classList.add("alert-success");
cardMessages.classList.remove("alert-danger");
cardMessages.classList.remove("d-none");
cardMessages.innerText = "Trasaction successful";
}
export function stripeInit(pubKey) {
stripe = Stripe(pubKey);
(function () {
// create stripe & elements
const elements = stripe.elements();
const style = {
base: {
fontSize: '16px',
lineHeight: '24px',
}
};
// create card entry
card = elements.create('card', {
style: style,
hidePostalCode: true,
});
card.mount("#card-element");
// check for input errors
card.addEventListener('change', function (event) {
var displayError = document.getElementById("card-errors");
if (event.error) {
displayError.classList.remove('d-none');
displayError.textContent = event.error.message;
} else {
displayError.classList.add('d-none');
displayError.textContent = "";
}
});
})();
}

40
static/js/stripe-plan.js Normal file
View File

@ -0,0 +1,40 @@
import {
hidePayButton,
showPayButton,
showCardError,
showCardSuccess,
stripe,
card,
processing,
} from './common.js';
export function val() {
let form = document.getElementById("charge_form");
if (form.checkValidity() === false) {
this.event.preventDefault();
this.event.stopPropagation();
form.classList.add("was-validated");
return;
}
form.classList.add("was-validated");
hidePayButton();
let amountToCharge = document.getElementById("amount").value;
stripe.createPaymentMethod({
type: 'card',
card: card,
billing_details: {
email: document.getElementById("cardholder-email").value,
},
}).then(stripePaymentMethodHandler);
}
function stripePaymentMethodHandler(result) {
if (result.error) {
showCardError(result.error.message);
} else {
// create a customer and subscribe to plan
}
}

View File

@ -1,34 +1,14 @@
let card; import {
let stripe; hidePayButton,
const cardMessages = document.getElementById("card-messages"); showPayButton,
const payButton = document.getElementById("pay-button"); showCardError,
const processing = document.getElementById("processing-payment"); showCardSuccess,
stripe,
card,
processing,
} from './common.js';
function hidePayButton() { export function val(api) {
payButton.classList.add("d-none");
processing.classList.remove("d-none");
}
function showPayButton() {
payButton.classList.remove("d-none");
processing.classList.add("d-none");
}
function showCardError(msg) {
cardMessages.classList.add("alert-danger");
cardMessages.classList.remove("alert-success");
cardMessages.classList.remove("d-none");
cardMessages.innerText = msg;
}
function showCardSuccess() {
cardMessages.classList.add("alert-success");
cardMessages.classList.remove("alert-danger");
cardMessages.classList.remove("d-none");
cardMessages.innerText = "Trasaction successful";
}
function val(api) {
let form = document.getElementById("charge_form"); let form = document.getElementById("charge_form");
if (form.checkValidity() === false) { if (form.checkValidity() === false) {
@ -97,36 +77,3 @@ function val(api) {
}); });
} }
function stripeInit(pubKey) {
stripe = Stripe(pubKey);
(function () {
// create stripe & elements
const elements = stripe.elements();
const style = {
base: {
fontSize: '16px',
lineHeight: '24px',
}
};
// create card entry
card = elements.create('card', {
style:style,
hidePostalCode: true,
});
card.mount("#card-element");
// check for input errors
card.addEventListener('change', function(event) {
var displayError = document.getElementById("card-errors");
if (event.error) {
displayError.classList.remove('d-none');
displayError.textContent = event.error.message;
} else {
displayError.classList.add('d-none');
displayError.textContent = "";
}
});
})();
}