Browse Source

Add code for users to unsubscribe from settings

Also embeds /assets, /migrations, and views/ directories in Go binary to
make deployment simpler.
master
Immanuel Onyeka 7 months ago
parent
commit
9f31c024f5
4 changed files with 85 additions and 14 deletions
  1. +7
    -2
      components/settings.vue
  2. +2
    -3
      components/unsubscribe.vue
  3. +1
    -0
      migrations/0_29092022_setup_tables.sql
  4. +75
    -9
      skouter.go

+ 7
- 2
components/settings.vue View File

@@ -54,7 +54,9 @@
<section class="form inputs special"> <section class="form inputs special">
<h3>Subscriptions</h3> <h3>Subscriptions</h3>
<label for="">Standard Plan ($49/month)</label> <label for="">Standard Plan ($49/month)</label>
<button @click="() => unsubing = true">Unsubscribe</button>
<button @click="() => unsubing = true">
{{props.user.sub.cancelAtEnd ? "Subscribe" : "Unsubscribe"}}
</button>
<label for="">Newsletter</label> <label for="">Newsletter</label>
<button @click="changeNewsSub"> <button @click="changeNewsSub">
{{props.user.newsletter ? "Unsubscribe" : "Subscribe"}} {{props.user.newsletter ? "Unsubscribe" : "Subscribe"}}
@@ -81,7 +83,10 @@
<button>Confirm</button> <button>Confirm</button>
</Dialog> </Dialog>


<UnsubPrompt v-if="unsubing" :token="token" @close="() => unsubing = false"/>
<UnsubPrompt v-if="unsubing" :token="token"
@close="() => unsubing = false"
@cancelSub="() => { $emit('updateUser'); unsubing = false }"
/>


</div> </div>
</template> </template>


+ 2
- 3
components/unsubscribe.vue View File

@@ -11,11 +11,10 @@
<script setup> <script setup>
import Dialog from "./dialog.vue" import Dialog from "./dialog.vue"


const emit = defineEmits(['close'])
const emit = defineEmits(['close', 'cancelSub'])
const props = defineProps(['token']) const props = defineProps(['token'])


function unsubscribe() { function unsubscribe() {
console.log(props.user)
fetch(`/api/user/unsubscribe`, fetch(`/api/user/unsubscribe`,
{method: 'GET', {method: 'GET',
headers: { headers: {
@@ -23,7 +22,7 @@ function unsubscribe() {
"Authorization": `Bearer ${props.token}`, "Authorization": `Bearer ${props.token}`,
}, },
}).then(resp => { }).then(resp => {
if (resp.ok) emit('updateUser')
if (resp.ok) emit('cancelSub')
}) })
} }
</script> </script>

+ 1
- 0
migrations/0_29092022_setup_tables.sql View File

@@ -77,6 +77,7 @@ CREATE TABLE subscription (
payment_status VARCHAR(50) NOT NULL, payment_status VARCHAR(50) NOT NULL,
current_period_end INT DEFAULT 0, current_period_end INT DEFAULT 0,
current_period_start INT DEFAULT 0, current_period_start INT DEFAULT 0,
cancel_at_end BOOL DEFAULT 0,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
FOREIGN KEY (user_id) REFERENCES user(id) FOREIGN KEY (user_id) REFERENCES user(id)
); );


+ 75
- 9
skouter.go View File

@@ -35,6 +35,7 @@ import (
"image" "image"
_ "image/jpeg" _ "image/jpeg"
"image/png" "image/png"
"embed"
) )


type Config struct { type Config struct {
@@ -71,6 +72,7 @@ type Subscription struct {
PriceId string `json:"priceId"` PriceId string `json:"priceId"`
Start int `json:"start"` Start int `json:"start"`
End int `json:"end"` End int `json:"end"`
CancelAtEnd bool `json:"cancelAtEnd"`
ClientSecret string `json:"clientSecret,omitempty"` ClientSecret string `json:"clientSecret,omitempty"`
PaymentStatus string `json:"paymentStatus"` PaymentStatus string `json:"paymentStatus"`
Status string `json:"status"` Status string `json:"status"`
@@ -302,6 +304,15 @@ var hookKeys = HookKeys{
SubDeleted: "", SubDeleted: "",
} }


//go:embed assets
var assets embed.FS

//go:embed migrations
var migrations embed.FS

//go:embed views
var views embed.FS

var standardPriceId = "price_1OZLK9BPMoXn2pf9kuTAf8rs" var standardPriceId = "price_1OZLK9BPMoXn2pf9kuTAf8rs"


// Used to validate claim in JWT token body. Checks if user id is greater than // Used to validate claim in JWT token body. Checks if user id is greater than
@@ -366,7 +377,7 @@ func cachePdf(name string) Page {
"views/report/summary.tpl", "views/report/summary.tpl",
"views/report/comparison.tpl"} "views/report/comparison.tpl"}


tpl := template.Must(template.New("master.tpl").Funcs(fm).ParseFiles(p...))
tpl := template.Must(template.New("master.tpl").Funcs(fm).ParseFS(views, p...))
return Page{tpl: tpl, Title: "", Name: name} return Page{tpl: tpl, Title: "", Name: name}
} }


@@ -1395,8 +1406,10 @@ func querySub(db *sql.DB, id int) (Subscription, error) {
customer_id, customer_id,
current_period_end, current_period_end,
current_period_start, current_period_start,
cancel_at_end,
client_secret, client_secret,
payment_status
payment_status,
status
FROM subscription WHERE id = ? FROM subscription WHERE id = ?
` `
row := db.QueryRow(query, id) row := db.QueryRow(query, id)
@@ -1408,8 +1421,10 @@ func querySub(db *sql.DB, id int) (Subscription, error) {
&s.CustomerId, &s.CustomerId,
&s.End, &s.End,
&s.Start, &s.Start,
&s.CancelAtEnd,
&s.ClientSecret, &s.ClientSecret,
&s.PaymentStatus, &s.PaymentStatus,
&s.Status,
) )


return s, err return s, err
@@ -1520,7 +1535,7 @@ func insertUser(db *sql.DB, user User) (int, error) {
) )
VALUES (?, ?, ?, sha2(?, 256), ?, ?, ?, ?, ?, ?, VALUES (?, ?, ?, sha2(?, 256), ?, ?, ?, ?, ?, ?,
CASE @b := ? WHEN 0 THEN NULL ELSE @b END, CASE @b := ? WHEN 0 THEN NULL ELSE @b END,
?, NOW(), NOW())
?, ?, NOW(), NOW())
RETURNING id RETURNING id
` `


@@ -1564,11 +1579,12 @@ func (sub *Subscription) insertSub(db *sql.DB) (error) {
price_id, price_id,
current_period_end, current_period_end,
current_period_start, current_period_start,
cancel_at_end,
client_secret, client_secret,
payment_status, payment_status,
status status
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id RETURNING id
` `
@@ -1579,6 +1595,7 @@ func (sub *Subscription) insertSub(db *sql.DB) (error) {
sub.PriceId, sub.PriceId,
sub.End, sub.End,
sub.Start, sub.Start,
sub.CancelAtEnd,
sub.ClientSecret, sub.ClientSecret,
sub.PaymentStatus, sub.PaymentStatus,
sub.Status, sub.Status,
@@ -1598,8 +1615,9 @@ func (sub *Subscription) updateSub(db *sql.DB) error {
@b := ? WHEN 0 THEN current_period_end ELSE @b END, @b := ? WHEN 0 THEN current_period_end ELSE @b END,
current_period_start = CASE current_period_start = CASE
@c := ? WHEN 0 THEN current_period_start ELSE @c END, @c := ? WHEN 0 THEN current_period_start ELSE @c END,
payment_status = CASE @d := ? WHEN '' THEN client_secret ELSE @d END,
status = CASE @e := ? WHEN '' THEN client_secret ELSE @e END
payment_status = CASE @d := ? WHEN '' THEN payment_status ELSE @d END,
status = CASE @e := ? WHEN '' THEN status ELSE @e END,
cancel_at_end = ?
WHERE id = ? WHERE id = ?
` `
@@ -1609,8 +1627,10 @@ func (sub *Subscription) updateSub(db *sql.DB) error {
sub.Start, sub.Start,
sub.PaymentStatus, sub.PaymentStatus,
sub.Status, sub.Status,
sub.CancelAtEnd,
sub.Id, sub.Id,
) )

if err != nil { return err } if err != nil { return err }


return err return err
@@ -3162,6 +3182,17 @@ func createTrialSubscription(cid string) (*stripe.Subscription, error) {
return s, err return s, err
} }


// Cancel or uncancel subscription at the end of the current billing cycle.
func (sub *Subscription) CancelSubscription(cancel bool) (*stripe.Subscription, error) {

subscriptionParams := &stripe.SubscriptionParams{
CancelAtPeriodEnd: stripe.Bool(cancel),
}
s, err := subscription.Update(sub.StripeId, subscriptionParams)
return s, err
}

func ( user *User ) SyncSub( sub *stripe.Subscription, db *sql.DB ) error { func ( user *User ) SyncSub( sub *stripe.Subscription, db *sql.DB ) error {
var err error var err error
@@ -3174,6 +3205,7 @@ func ( user *User ) SyncSub( sub *stripe.Subscription, db *sql.DB ) error {
user.Sub.ClientSecret = sub.LatestInvoice.PaymentIntent.ClientSecret user.Sub.ClientSecret = sub.LatestInvoice.PaymentIntent.ClientSecret
user.Sub.PaymentStatus = string(sub.LatestInvoice.PaymentIntent.Status) user.Sub.PaymentStatus = string(sub.LatestInvoice.PaymentIntent.Status)
user.Sub.Status = string(sub.Status) user.Sub.Status = string(sub.Status)
user.Sub.CancelAtEnd = sub.CancelAtPeriodEnd
if user.Sub.Id != 0 { if user.Sub.Id != 0 {
err = user.Sub.insertSub(db) err = user.Sub.insertSub(db)
@@ -3312,6 +3344,36 @@ func trialSubscribe(w http.ResponseWriter, db *sql.DB, r *http.Request) {
json.NewEncoder(w).Encode(user.Sub) json.NewEncoder(w).Encode(user.Sub)
} }


// Sets Stripe subscription to cancel automatically at end of current period,
// and updates subscription's 'CancelAtEnd' to true. If user's subscription is
// already canceled, it will uncancel the subscription.
func unsubscribe(w http.ResponseWriter, db *sql.DB, r *http.Request) {
stripe.Key = os.Getenv("STRIPE_SECRET_KEY")
claims, err := getClaims(r)
user, err := queryUser(db, claims.Id)

if err != nil {
w.WriteHeader(422)
return
}
user.querySub(db)
_, err = user.Sub.CancelSubscription(!user.Sub.CancelAtEnd)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
user.Sub.CancelAtEnd = !user.Sub.CancelAtEnd
err = user.Sub.updateSub(db)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}

// A successful subscription payment should be confirmed by Stripe and // A successful subscription payment should be confirmed by Stripe and
// Updated through this hook. // Updated through this hook.
func invoicePaid(w http.ResponseWriter, db *sql.DB, r *http.Request) { func invoicePaid(w http.ResponseWriter, db *sql.DB, r *http.Request) {
@@ -3698,6 +3760,10 @@ func api(w http.ResponseWriter, r *http.Request) {
r.Method == http.MethodPost && r.Method == http.MethodPost &&
guard(r, 1): guard(r, 1):
subscribe(w, db, r) subscribe(w, db, r)
case match(p, "/api/user/unsubscribe", &args) &&
r.Method == http.MethodGet &&
guard(r, 1):
unsubscribe(w, db, r)
case match(p, "/api/user/trial", &args) && case match(p, "/api/user/trial", &args) &&
r.Method == http.MethodPost && r.Method == http.MethodPost &&
guard(r, 1): guard(r, 1):
@@ -3813,7 +3879,7 @@ func serve() {
} }


func dbReset(db *sql.DB) { func dbReset(db *sql.DB) {
b, err := os.ReadFile("migrations/reset.sql")
b, err := migrations.ReadFile("migrations/reset.sql")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -3823,7 +3889,7 @@ func dbReset(db *sql.DB) {
log.Fatal(err) log.Fatal(err)
} }


b, err = os.ReadFile("migrations/0_29092022_setup_tables.sql")
b, err = migrations.ReadFile("migrations/0_29092022_setup_tables.sql")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -4135,7 +4201,7 @@ func dev(args []string) {
} }


func check(args []string) { func check(args []string) {
files := http.FileServer(http.Dir(""))
files := http.FileServerFS(assets)


http.Handle("/assets/", files) http.Handle("/assets/", files)
http.HandleFunc("/", checkPdf) http.HandleFunc("/", checkPdf)


Loading…
Cancel
Save