123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194 |
- package controller
- import (
- "context"
- "fmt"
- "github.com/ente-io/museum/pkg/controller/commonbilling"
- "github.com/prometheus/common/log"
- "strconv"
- "strings"
- "github.com/ente-io/stacktrace"
- "github.com/gin-contrib/requestid"
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/spf13/viper"
- "github.com/awa/go-iap/appstore"
- "github.com/ente-io/museum/ente"
- "github.com/ente-io/museum/pkg/repo"
- "github.com/ente-io/museum/pkg/utils/array"
- )
- // AppStoreController provides abstractions for handling billing on AppStore
- type AppStoreController struct {
- AppStoreClient appstore.Client
- BillingRepo *repo.BillingRepository
- FileRepo *repo.FileRepository
- UserRepo *repo.UserRepository
- BillingPlansPerCountry ente.BillingPlansPerCountry
- CommonBillCtrl *commonbilling.Controller
- // appStoreSharedPassword is the password to be used to access AppStore APIs
- appStoreSharedPassword string
- }
- // Return a new instance of AppStoreController
- func NewAppStoreController(
- plans ente.BillingPlansPerCountry,
- billingRepo *repo.BillingRepository,
- fileRepo *repo.FileRepository,
- userRepo *repo.UserRepository,
- commonBillCtrl *commonbilling.Controller,
- ) *AppStoreController {
- appleSharedSecret := viper.GetString("apple.shared-secret")
- return &AppStoreController{
- AppStoreClient: *appstore.New(),
- BillingRepo: billingRepo,
- FileRepo: fileRepo,
- UserRepo: userRepo,
- BillingPlansPerCountry: plans,
- appStoreSharedPassword: appleSharedSecret,
- CommonBillCtrl: commonBillCtrl,
- }
- }
- var SubsUpdateNotificationTypes = []string{string(appstore.NotificationTypeDidChangeRenewalStatus), string(appstore.NotificationTypeCancel), string(appstore.NotificationTypeDidRevoke)}
- // HandleNotification handles an AppStore notification
- func (c *AppStoreController) HandleNotification(ctx *gin.Context, notification appstore.SubscriptionNotification) error {
- logger := logrus.WithFields(logrus.Fields{
- "req_id": requestid.Get(ctx),
- })
- purchase, err := c.verifyAppStoreSubscription(notification.UnifiedReceipt.LatestReceipt)
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- latestReceiptInfo := c.getLatestReceiptInfo(purchase.LatestReceiptInfo)
- if latestReceiptInfo.TransactionID == latestReceiptInfo.OriginalTransactionID && !array.StringInList(string(notification.NotificationType), SubsUpdateNotificationTypes) {
- var logMsg = fmt.Sprintf("Ignoring notification of type %s", notification.NotificationType)
- if notification.NotificationType != appstore.NotificationTypeInitialBuy {
- // log unexpected notification types
- logger.Error(logMsg)
- } else {
- logger.Info(logMsg)
- }
- // First subscription, no user to link to
- return nil
- }
- subscription, err := c.BillingRepo.GetSubscriptionForTransaction(latestReceiptInfo.OriginalTransactionID, ente.AppStore)
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- expiryTimeInMillis, _ := strconv.ParseInt(latestReceiptInfo.ExpiresDate.ExpiresDateMS, 10, 64)
- if latestReceiptInfo.ProductID == subscription.ProductID && expiryTimeInMillis*1000 < subscription.ExpiryTime {
- // Outdated notification, no-op
- } else {
- if latestReceiptInfo.ProductID != subscription.ProductID {
- var newPlan ente.BillingPlan
- plans := c.BillingPlansPerCountry["EU"] // Country code is irrelevant since Storage will be the same for a given subscriptionID
- for _, plan := range plans {
- if plan.IOSID == latestReceiptInfo.ProductID {
- newPlan = plan
- break
- }
- }
- if newPlan.Storage < subscription.Storage { // Downgrade
- canDowngrade, canDowngradeErr := c.CommonBillCtrl.CanDowngradeToGivenStorage(newPlan.Storage, subscription.UserID)
- if canDowngradeErr != nil {
- return stacktrace.Propagate(canDowngradeErr, "")
- }
- if !canDowngrade {
- return stacktrace.Propagate(ente.ErrCannotDowngrade, "")
- }
- log.Info("Usage is good")
- }
- newSubscription := ente.Subscription{
- Storage: newPlan.Storage,
- ExpiryTime: expiryTimeInMillis * 1000,
- ProductID: latestReceiptInfo.ProductID,
- PaymentProvider: ente.AppStore,
- OriginalTransactionID: latestReceiptInfo.OriginalTransactionID,
- Attributes: ente.SubscriptionAttributes{LatestVerificationData: notification.UnifiedReceipt.LatestReceipt},
- }
- err = c.BillingRepo.ReplaceSubscription(
- subscription.ID,
- newSubscription,
- )
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- } else {
- if notification.NotificationType == appstore.NotificationTypeDidChangeRenewalStatus {
- err := c.BillingRepo.UpdateSubscriptionCancellationStatus(subscription.UserID, notification.AutoRenewStatus == "false")
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- } else if notification.NotificationType == appstore.NotificationTypeCancel || notification.NotificationType == appstore.NotificationTypeDidRevoke {
- err := c.BillingRepo.UpdateSubscriptionCancellationStatus(subscription.UserID, true)
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- }
- err = c.BillingRepo.UpdateSubscriptionExpiryTime(subscription.ID, expiryTimeInMillis*1000)
- if err != nil {
- return stacktrace.Propagate(err, "")
- }
- }
- }
- err = c.BillingRepo.LogAppStorePush(subscription.UserID, notification, *purchase)
- return stacktrace.Propagate(err, "")
- }
- // GetVerifiedSubscription verifies and returns the verified subscription
- func (c *AppStoreController) GetVerifiedSubscription(userID int64, productID string, verificationData string) (ente.Subscription, error) {
- var s ente.Subscription
- s.UserID = userID
- s.ProductID = productID
- s.PaymentProvider = ente.AppStore
- s.Attributes.LatestVerificationData = verificationData
- plans := c.BillingPlansPerCountry["EU"] // Country code is irrelevant since Storage will be the same for a given subscriptionID
- response, err := c.verifyAppStoreSubscription(verificationData)
- if err != nil {
- return ente.Subscription{}, stacktrace.Propagate(err, "")
- }
- for _, plan := range plans {
- if plan.IOSID == productID {
- s.Storage = plan.Storage
- break
- }
- }
- latestReceiptInfo := c.getLatestReceiptInfo(response.LatestReceiptInfo)
- s.OriginalTransactionID = latestReceiptInfo.OriginalTransactionID
- expiryTime, _ := strconv.ParseInt(latestReceiptInfo.ExpiresDate.ExpiresDateMS, 10, 64)
- s.ExpiryTime = expiryTime * 1000
- return s, nil
- }
- // VerifyAppStoreSubscription verifies an AppStore subscription
- func (c *AppStoreController) verifyAppStoreSubscription(verificationData string) (*appstore.IAPResponse, error) {
- iapRequest := appstore.IAPRequest{
- ReceiptData: verificationData,
- Password: c.appStoreSharedPassword,
- }
- response := &appstore.IAPResponse{}
- context := context.Background()
- err := c.AppStoreClient.Verify(context, iapRequest, response)
- if err != nil {
- return nil, stacktrace.Propagate(err, "")
- }
- if response.Status != 0 {
- return nil, ente.ErrBadRequest
- }
- return response, nil
- }
- func (c *AppStoreController) getLatestReceiptInfo(receiptInfo []appstore.InApp) appstore.InApp {
- latestReceiptInfo := receiptInfo[0]
- for _, receiptInfo := range receiptInfo {
- if strings.Compare(latestReceiptInfo.ExpiresDate.ExpiresDateMS, receiptInfo.ExpiresDate.ExpiresDateMS) < 0 {
- latestReceiptInfo = receiptInfo
- }
- }
- return latestReceiptInfo
- }
|