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

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 4 месяцев назад
Родитель
Сommit
9f31c024f5
4 измененных файлов: 85 добавлений и 14 удалений
  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 Просмотреть файл

@@ -54,7 +54,9 @@
<section class="form inputs special">
<h3>Subscriptions</h3>
<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>
<button @click="changeNewsSub">
{{props.user.newsletter ? "Unsubscribe" : "Subscribe"}}
@@ -81,7 +83,10 @@
<button>Confirm</button>
</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>
</template>


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

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

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

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

+ 1
- 0
migrations/0_29092022_setup_tables.sql Просмотреть файл

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


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

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

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

// 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/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}
}

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

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

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

if err != nil { return err }

return err
@@ -3162,6 +3182,17 @@ func createTrialSubscription(cid string) (*stripe.Subscription, error) {
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 {
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.PaymentStatus = string(sub.LatestInvoice.PaymentIntent.Status)
user.Sub.Status = string(sub.Status)
user.Sub.CancelAtEnd = sub.CancelAtPeriodEnd
if user.Sub.Id != 0 {
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)
}

// 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
// Updated through this hook.
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 &&
guard(r, 1):
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) &&
r.Method == http.MethodPost &&
guard(r, 1):
@@ -3813,7 +3879,7 @@ func serve() {
}

func dbReset(db *sql.DB) {
b, err := os.ReadFile("migrations/reset.sql")
b, err := migrations.ReadFile("migrations/reset.sql")
if err != nil {
log.Fatal(err)
}
@@ -3823,7 +3889,7 @@ func dbReset(db *sql.DB) {
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 {
log.Fatal(err)
}
@@ -4135,7 +4201,7 @@ func dev(args []string) {
}

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

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


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