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