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