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