/ subscription.go
subscription.go
1 package main 2 3 import ( 4 "encoding/json" 5 "log" 6 "strconv" 7 "time" 8 9 "github.com/google/uuid" 10 "github.com/maxence-charriere/go-app/v10/pkg/app" 11 shell "github.com/stateless-minds/go-ipfs-api" 12 ) 13 14 const dbSubscription = "subscription" 15 16 // subscription is a component that holds cyber-gubi. A component is a 17 // customizable, independent, and reusable UI element. It is created by 18 // embedding app.Compo into a struct. 19 type subscription struct { 20 app.Compo 21 sh *shell.Shell 22 loggedIn bool 23 userID string 24 wallet Wallet 25 plans []Plan 26 subscriptions []Subscription 27 subscribed bool 28 observer app.Value 29 callback app.Func 30 lastIndex int 31 indexStep int 32 } 33 34 func (s *subscription) OnMount(ctx app.Context) { 35 sh := shell.NewShell("localhost:5001") 36 s.sh = sh 37 s.indexStep = 99 38 39 ctx.GetState("loggedIn", &s.loggedIn) 40 if !s.loggedIn { 41 ctx.Navigate("/auth") 42 } 43 44 s.callback = app.FuncOf(func(this app.Value, args []app.Value) interface{} { 45 entries := args[0] 46 for i := 0; i < entries.Length(); i++ { 47 entry := entries.Index(i) 48 if entry.Get("isIntersecting").Bool() { 49 // Element is visible - do something 50 s.getPlans(ctx) 51 } 52 } 53 return nil 54 }) 55 56 // Select the root element by class name 57 rootElement := app.Window().Get("document").Call("querySelector", ".list") 58 59 options := map[string]interface{}{ 60 "root": rootElement, 61 "rootMargin": "0px", 62 "threshold": 1, 63 } 64 65 observerConstructor := app.Window().Get("IntersectionObserver") 66 s.observer = observerConstructor.New(s.callback, options) 67 68 ctx.GetState("userID", &s.userID) 69 ctx.GetState("balance", &s.wallet) 70 71 s.getPlans(ctx) 72 } 73 74 func (s *subscription) OnUpdate(ctx app.Context) { 75 // Wrap your observation logic in a Go function 76 callback := func() { 77 target := app.Window().GetElementByID("last-item") 78 if !target.IsNull() && !target.IsUndefined() { 79 s.observer.Call("disconnect") 80 s.observer.Call("observe", target) 81 } 82 } 83 84 var goFunc app.Func 85 86 // Wrap callback as JS function 87 goFunc = app.FuncOf(func(this app.Value, args []app.Value) interface{} { 88 callback() 89 goFunc.Release() // release after call to avoid leaks 90 return nil 91 }) 92 93 // Call JS setTimeout with delay 10ms 94 app.Window().Call("goAppSetTimeout", goFunc, 100) 95 } 96 97 func (s *subscription) OnDismount(ctx app.Context) { 98 s.observer.Call("disconnect") 99 s.callback.Release() 100 } 101 102 func (s *subscription) getPlans(ctx app.Context) { 103 ctx.Async(func() { 104 rangeStart := strconv.Itoa(s.lastIndex) 105 rangeEnd := strconv.Itoa(s.lastIndex + s.indexStep) 106 p, err := s.sh.OrbitDocsQuery(dbPlan, "all", "range="+rangeStart+"-"+rangeEnd) 107 if err != nil { 108 log.Fatal(err) 109 } 110 111 plans := []Plan{} 112 113 if len(p) != 0 { 114 err = json.Unmarshal(p, &plans) // Unmarshal the byte slice directly 115 if err != nil { 116 log.Fatal(err) 117 } 118 } else { 119 s.OnDismount(ctx) 120 } 121 122 ctx.Dispatch(func(ctx app.Context) { 123 s.plans = append(s.plans, plans...) 124 s.deleteExpiredSubscriptions(ctx) 125 s.lastIndex = s.lastIndex + 1 + s.indexStep 126 s.OnUpdate(ctx) 127 }) 128 }) 129 } 130 131 func (s *subscription) getSubscriptions(ctx app.Context) { 132 ctx.Async(func() { 133 subs, err := s.sh.OrbitDocsQuery(dbSubscription, "user_id", s.userID) 134 if err != nil { 135 log.Fatal(err) 136 } 137 138 subscriptions := []Subscription{} 139 140 if len(subs) != 0 { 141 err = json.Unmarshal(subs, &subscriptions) // Unmarshal the byte slice directly 142 if err != nil { 143 log.Fatal(err) 144 } 145 } 146 147 ctx.Dispatch(func(ctx app.Context) { 148 s.subscriptions = subscriptions 149 }) 150 }) 151 } 152 153 func (s *subscription) getBalance(userID string) (wallet Wallet, err error) { 154 b, err := s.sh.OrbitDocsQuery(dbWallet, "_id", userID) 155 if err != nil { 156 return Wallet{}, err 157 } 158 159 if len(b) == 0 { 160 return Wallet{}, err 161 } 162 163 wallets := []Wallet{} 164 165 err = json.Unmarshal(b, &wallets) // Unmarshal the byte slice directly 166 if err != nil { 167 return Wallet{}, err 168 } 169 170 return wallets[0], nil 171 } 172 173 func (s *subscription) updateBalance(userID string, balance, income int, date string) error { 174 wallet := Wallet{ 175 ID: userID, 176 Balance: balance, 177 Income: income, 178 LastReceived: date, 179 } 180 181 walletJSON, err := json.Marshal(wallet) 182 if err != nil { 183 return err 184 } 185 186 err = s.sh.OrbitDocsPut(dbWallet, walletJSON) 187 if err != nil { 188 return err 189 } 190 191 return nil 192 } 193 194 func (s *subscription) storeTransaction(transaction Transaction) error { 195 transactionJSON, err := json.Marshal(transaction) 196 if err != nil { 197 return err 198 } 199 200 err = s.sh.OrbitDocsPut(dbTransaction, transactionJSON) 201 if err != nil { 202 return err 203 } 204 205 return nil 206 } 207 208 func (s *subscription) storeSubscription(subscription Subscription) error { 209 subscriptionJSON, err := json.Marshal(subscription) 210 if err != nil { 211 return err 212 } 213 214 err = s.sh.OrbitDocsPut(dbSubscription, subscriptionJSON) 215 if err != nil { 216 return err 217 } 218 219 return nil 220 } 221 222 func (s *subscription) deleteExpiredSubscriptions(ctx app.Context) { 223 ctx.Async(func() { 224 s.sh.DeleteExpiredSubscriptions() 225 226 ctx.Dispatch(func(ctx app.Context) { 227 s.getSubscriptions(ctx) 228 }) 229 }) 230 } 231 232 func (s *subscription) deleteSubscription(id string) error { 233 err := s.sh.OrbitDocsDelete(dbSubscription, id) 234 if err != nil { 235 return err 236 } 237 238 return nil 239 } 240 241 func (s *subscription) doSubscribe(ctx app.Context, e app.Event) { 242 e.PreventDefault() 243 pid := ctx.JSSrc().Get("value").String() 244 planID, err := strconv.Atoi(pid) 245 if err != nil { 246 log.Fatal(err) 247 } 248 249 plan := s.plans[planID] 250 251 subscription := Subscription{ 252 ID: uuid.NewString(), 253 PlanID: plan.ID, 254 UserID: s.userID, 255 Price: plan.Price, 256 StartDate: time.Now(), 257 EndDate: time.Now().AddDate(0, 1, 0), 258 } 259 260 // store subscription 261 s.storeSubscription(subscription) 262 263 // store transaction 264 transaction := Transaction{} 265 transaction.ID = uuid.NewString() 266 transaction.SenderID = s.userID 267 transaction.ReceiverID = plan.CreatedBy 268 transaction.Timestamp = time.Now() 269 transaction.Date = strconv.Itoa(time.Now().Year()) + "/" + strconv.Itoa(int(time.Now().Month())) 270 transaction.ProductsServices = []ProductService{ 271 { 272 ID: plan.ID, 273 Name: plan.Name, 274 Price: plan.Price, 275 Amount: 1, 276 }, 277 } 278 transaction.TotalCost = plan.Price 279 280 if s.wallet.Balance-transaction.TotalCost < 0 { 281 ctx.Notifications().New(app.Notification{ 282 Title: "Error", 283 Body: "Not enough funds.", 284 }) 285 return 286 } 287 // update sender balance 288 err = s.updateBalance(s.userID, s.wallet.Balance-transaction.TotalCost, s.wallet.Income, s.wallet.LastReceived) 289 if err != nil { 290 log.Fatal(err) 291 } 292 // get receiver balance 293 receiverBalance, err := s.getBalance(transaction.ReceiverID) 294 if err != nil { 295 log.Fatal(err) 296 } 297 // update receiver balance 298 err = s.updateBalance(transaction.ReceiverID, receiverBalance.Balance+transaction.TotalCost, receiverBalance.Income, receiverBalance.LastReceived) 299 if err != nil { 300 // rollback sender balance 301 err := s.updateBalance(s.userID, s.wallet.Balance+transaction.TotalCost, s.wallet.Income, s.wallet.LastReceived) 302 if err != nil { 303 log.Fatal(err) 304 } 305 err = s.deleteSubscription(subscription.ID) 306 if err != nil { 307 log.Fatal(err) 308 } 309 return 310 } 311 // store transaction 312 err = s.storeTransaction(transaction) 313 if err != nil { 314 // rollback sender balance 315 err = s.updateBalance(s.userID, s.wallet.Balance+transaction.TotalCost, s.wallet.Income, s.wallet.LastReceived) 316 if err != nil { 317 log.Fatal(err) 318 } 319 // rollback receiver balance 320 err = s.updateBalance(transaction.ReceiverID, receiverBalance.Balance-transaction.TotalCost, receiverBalance.Income, receiverBalance.LastReceived) 321 if err != nil { 322 log.Fatal(err) 323 } 324 err = s.deleteSubscription(subscription.ID) 325 if err != nil { 326 log.Fatal(err) 327 } 328 return 329 } 330 331 s.wallet.Balance = s.wallet.Balance - transaction.TotalCost 332 s.subscriptions = append(s.subscriptions, subscription) 333 ctx.Update() 334 335 ctx.Notifications().New(app.Notification{ 336 Title: "Success", 337 Body: "Subscription successful!", 338 }) 339 } 340 341 // The Render method is where the component appearance is defined. Here, a 342 // subscription form is displayed. 343 func (s *subscription) Render() app.UI { 344 return app.Div().Class("container").Body( 345 app.Div().Class("mobile").Body( 346 app.Div().Class("header").Body( 347 newNav(), 348 app.Div().Class("header-summary").Body( 349 app.Span().Class("logo").Text("cyber-gubi"), 350 app.Div().Class("summary-text").Body( 351 app.Span().Text("Balance"), 352 ), 353 app.Div().Class("summary-balance").Body( 354 app.Span().Text(strconv.Itoa(s.wallet.Balance/100)+" GUBI"), 355 ), 356 ), 357 ), 358 app.Div().ID("content").Body( 359 app.Div().Class("card").Body( 360 app.Div().Class("upper-row upper-row-sub").Body( 361 app.Div().Class("card-item").Body( 362 app.Span().Class("span-header-sub").Text("Subscriptions"), 363 ), 364 ), 365 ), 366 app.Div().Class("list").Body( 367 app.If(len(s.plans) == 0, func() app.UI { 368 return app.Div().Class("list-item").Body( 369 app.Span().Class("empty").Text("No plans yet"), 370 ).Style("pointer-events", "none") 371 }), 372 app.Range(s.plans).Slice(func(i int) app.UI { 373 s.subscribed = false 374 return app.If(i == len(s.plans)-1 && len(s.plans)%5 == 0, func() app.UI { 375 return app.Div().ID("last-item").Class("list-item").Body( 376 app.Div().Class("s-details").Body( 377 app.Div().Class("s-title").Body( 378 app.Span().Text(s.plans[i].Name), 379 ), 380 app.Div().Class("s-time").Body( 381 app.If(len(s.subscriptions) > 0, func() app.UI { 382 return app.Range(s.subscriptions).Slice(func(n int) app.UI { 383 return app.If(s.subscriptions[n].PlanID == s.plans[i].ID && s.subscriptions[n].UserID == s.userID, func() app.UI { 384 return app.If(time.Now().Before(s.subscriptions[n].EndDate), func() app.UI { 385 s.subscribed = true 386 return app.Div().Class("menu-btn menu-sub menu-subscribed").Body( 387 app.Button().Class("submit submit-sub").Type("submit").Text("Subscribed").Disabled(true), 388 ) 389 }) 390 }) 391 }) 392 }), 393 app.If(!s.subscribed, func() app.UI { 394 return app.Div().Class("menu-btn menu-sub").Body( 395 app.Button().Class("submit submit-sub").Type("submit").Text("Subscribe").Value(i).OnClick(s.doSubscribe), 396 ) 397 }), 398 ), 399 ), 400 app.Div().Class("s-price").Body( 401 app.Span().Text(strconv.Itoa(s.plans[i].Price/100)+" GUBI"), 402 ), 403 ) 404 }).Else(func() app.UI { 405 return app.Div().Class("list-item").Body( 406 app.Div().Class("s-details").Body( 407 app.Div().Class("s-title").Body( 408 app.Span().Text(s.plans[i].Name), 409 ), 410 app.Div().Class("s-time").Body( 411 app.If(len(s.subscriptions) > 0, func() app.UI { 412 return app.Range(s.subscriptions).Slice(func(n int) app.UI { 413 return app.If(s.subscriptions[n].PlanID == s.plans[i].ID && s.subscriptions[n].UserID == s.userID, func() app.UI { 414 return app.If(time.Now().Before(s.subscriptions[n].EndDate), func() app.UI { 415 s.subscribed = true 416 return app.Div().Class("menu-btn menu-sub menu-subscribed").Body( 417 app.Button().Class("submit submit-sub").Type("submit").Text("Subscribed").Disabled(true), 418 ) 419 }) 420 }) 421 }) 422 }), 423 app.If(!s.subscribed, func() app.UI { 424 return app.Div().Class("menu-btn menu-sub").Body( 425 app.Button().Class("submit submit-sub").Type("submit").Text("Subscribe").Value(i).OnClick(s.doSubscribe), 426 ) 427 }), 428 ), 429 ), 430 app.Div().Class("s-price").Body( 431 app.Span().Text(strconv.Itoa(s.plans[i].Price/100)+" GUBI"), 432 ), 433 ) 434 }) 435 }), 436 ), 437 ), 438 ), 439 ) 440 }