satellite/console: make paywall optional
Add a config so that some percent of users require credit cards / account balances in order to create a project or have a promotional coupon applied UI was updated to match needed paywall status At this point we decided not to use a field to store if a user is in an A/B test, and instead just use math to see if they're in a test. We decided to use MD5 (because its in Postgres too) and User UUID for that math. Change-Id: I0fcd80707dc29afc668632d078e1b5a7a24f3bb3
This commit is contained in:
parent
d30cb1ada2
commit
b265b7f555
@ -83,7 +83,8 @@ func setupPayments(log *zap.Logger, db satellite.DB) (*stripecoinpayments.Servic
|
|||||||
pc.CouponValue,
|
pc.CouponValue,
|
||||||
pc.CouponDuration,
|
pc.CouponDuration,
|
||||||
pc.CouponProjectLimit,
|
pc.CouponProjectLimit,
|
||||||
pc.MinCoinPayment)
|
pc.MinCoinPayment,
|
||||||
|
pc.PaywallProportion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseBillingPeriodFromString parses provided date string and returns corresponding time.Time.
|
// parseBillingPeriodFromString parses provided date string and returns corresponding time.Time.
|
||||||
|
@ -483,8 +483,9 @@ func (planet *Planet) newSatellites(count int, satelliteDatabases satellitedbtes
|
|||||||
ConversionRatesCycleInterval: defaultInterval,
|
ConversionRatesCycleInterval: defaultInterval,
|
||||||
ListingLimit: 100,
|
ListingLimit: 100,
|
||||||
},
|
},
|
||||||
CouponDuration: 2,
|
CouponDuration: 2,
|
||||||
CouponValue: 275,
|
CouponValue: 275,
|
||||||
|
PaywallProportion: 1,
|
||||||
},
|
},
|
||||||
Repairer: repairer.Config{
|
Repairer: repairer.Config{
|
||||||
MaxRepair: 10,
|
MaxRepair: 10,
|
||||||
|
@ -134,7 +134,8 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB,
|
|||||||
pc.CouponValue,
|
pc.CouponValue,
|
||||||
pc.CouponDuration,
|
pc.CouponDuration,
|
||||||
pc.CouponProjectLimit,
|
pc.CouponProjectLimit,
|
||||||
pc.MinCoinPayment)
|
pc.MinCoinPayment,
|
||||||
|
pc.PaywallProportion)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.Combine(err, peer.Close())
|
return nil, errs.Combine(err, peer.Close())
|
||||||
|
@ -538,7 +538,8 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
|
|||||||
pc.CouponValue,
|
pc.CouponValue,
|
||||||
pc.CouponDuration,
|
pc.CouponDuration,
|
||||||
pc.CouponProjectLimit,
|
pc.CouponProjectLimit,
|
||||||
pc.MinCoinPayment)
|
pc.MinCoinPayment,
|
||||||
|
pc.PaywallProportion)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.Combine(err, peer.Close())
|
return nil, errs.Combine(err, peer.Close())
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/zeebo/errs"
|
"github.com/zeebo/errs"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"storj.io/common/uuid"
|
||||||
"storj.io/storj/satellite/console"
|
"storj.io/storj/satellite/console"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -305,6 +306,34 @@ func (p *Payments) TokenDeposit(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PaywallEnabled returns is paywall enabled status.
|
||||||
|
func (p *Payments) PaywallEnabled(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
var err error
|
||||||
|
defer mon.Task()(&ctx)(&err)
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
reqID := vars["userId"]
|
||||||
|
|
||||||
|
if reqID == "" {
|
||||||
|
p.serveJSONError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := uuid.FromString(reqID)
|
||||||
|
if err != nil {
|
||||||
|
p.serveJSONError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
paywallEnabled := p.service.PaywallEnabled(userID)
|
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(paywallEnabled)
|
||||||
|
if err != nil {
|
||||||
|
p.log.Error("failed to write json paywall enabled response", zap.Error(ErrPaymentsAPI.Wrap(err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// serveJSONError writes JSON error to response output stream.
|
// serveJSONError writes JSON error to response output stream.
|
||||||
func (p *Payments) serveJSONError(w http.ResponseWriter, status int, err error) {
|
func (p *Payments) serveJSONError(w http.ResponseWriter, status int, err error) {
|
||||||
if status == http.StatusInternalServerError {
|
if status == http.StatusInternalServerError {
|
||||||
|
37
satellite/console/consoleweb/consoleapi/payments_test.go
Normal file
37
satellite/console/consoleweb/consoleapi/payments_test.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (C) 2020 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
package consoleapi_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"storj.io/common/testcontext"
|
||||||
|
"storj.io/common/uuid"
|
||||||
|
"storj.io/storj/private/testplanet"
|
||||||
|
"storj.io/storj/satellite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestPaywallEnabled ensures that the Paywall A/B test config works.
|
||||||
|
func TestPaywallEnabled(t *testing.T) {
|
||||||
|
lowUUID := uuid.UUID{0}
|
||||||
|
highUUID := uuid.UUID{255}
|
||||||
|
testplanet.Run(t, testplanet.Config{
|
||||||
|
SatelliteCount: 3, Reconfigure: testplanet.Reconfigure{
|
||||||
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||||
|
proportions := []float64{0, .5, 1}
|
||||||
|
config.Payments.PaywallProportion = proportions[index]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||||
|
assert.False(t, planet.Satellites[0].API.Console.Service.PaywallEnabled(highUUID))
|
||||||
|
assert.False(t, planet.Satellites[0].API.Console.Service.PaywallEnabled(lowUUID))
|
||||||
|
assert.False(t, planet.Satellites[1].API.Console.Service.PaywallEnabled(highUUID))
|
||||||
|
assert.True(t, planet.Satellites[1].API.Console.Service.PaywallEnabled(lowUUID))
|
||||||
|
assert.True(t, planet.Satellites[2].API.Console.Service.PaywallEnabled(highUUID))
|
||||||
|
assert.True(t, planet.Satellites[2].API.Console.Service.PaywallEnabled(lowUUID))
|
||||||
|
})
|
||||||
|
}
|
@ -90,7 +90,8 @@ func TestGrapqhlMutation(t *testing.T) {
|
|||||||
pc.CouponValue,
|
pc.CouponValue,
|
||||||
pc.CouponDuration,
|
pc.CouponDuration,
|
||||||
pc.CouponProjectLimit,
|
pc.CouponProjectLimit,
|
||||||
pc.MinCoinPayment)
|
pc.MinCoinPayment,
|
||||||
|
pc.PaywallProportion)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
service, err := console.NewService(
|
service, err := console.NewService(
|
||||||
|
@ -74,7 +74,8 @@ func TestGraphqlQuery(t *testing.T) {
|
|||||||
pc.CouponValue,
|
pc.CouponValue,
|
||||||
pc.CouponDuration,
|
pc.CouponDuration,
|
||||||
pc.CouponProjectLimit,
|
pc.CouponProjectLimit,
|
||||||
pc.MinCoinPayment)
|
pc.MinCoinPayment,
|
||||||
|
pc.PaywallProportion)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
service, err := console.NewService(
|
service, err := console.NewService(
|
||||||
|
@ -188,6 +188,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
|
|||||||
paymentsRouter.HandleFunc("/account", paymentController.SetupAccount).Methods(http.MethodPost)
|
paymentsRouter.HandleFunc("/account", paymentController.SetupAccount).Methods(http.MethodPost)
|
||||||
paymentsRouter.HandleFunc("/billing-history", paymentController.BillingHistory).Methods(http.MethodGet)
|
paymentsRouter.HandleFunc("/billing-history", paymentController.BillingHistory).Methods(http.MethodGet)
|
||||||
paymentsRouter.HandleFunc("/tokens/deposit", paymentController.TokenDeposit).Methods(http.MethodPost)
|
paymentsRouter.HandleFunc("/tokens/deposit", paymentController.TokenDeposit).Methods(http.MethodPost)
|
||||||
|
paymentsRouter.HandleFunc("/paywall-enabled/{userId}", paymentController.PaywallEnabled).Methods(http.MethodGet)
|
||||||
|
|
||||||
if server.config.StaticDir != "" {
|
if server.config.StaticDir != "" {
|
||||||
router.HandleFunc("/activation/", server.accountActivationHandler)
|
router.HandleFunc("/activation/", server.accountActivationHandler)
|
||||||
|
@ -174,9 +174,13 @@ func (paymentService PaymentsService) AddCreditCard(ctx context.Context, creditC
|
|||||||
return Error.Wrap(err)
|
return Error.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !paymentService.service.accounts.PaywallEnabled(auth.User.ID) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
err = paymentService.AddPromotionalCoupon(ctx, auth.User.ID)
|
err = paymentService.AddPromotionalCoupon(ctx, auth.User.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
paymentService.service.log.Debug(fmt.Sprintf("could not add promotional coupon sof user %s", auth.User.ID.String()), zap.Error(Error.Wrap(err)))
|
paymentService.service.log.Debug(fmt.Sprintf("could not add promotional coupon for user %s", auth.User.ID.String()), zap.Error(Error.Wrap(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -387,12 +391,14 @@ func (paymentService PaymentsService) PopulatePromotionalCoupons(ctx context.Con
|
|||||||
func (paymentService PaymentsService) AddPromotionalCoupon(ctx context.Context, userID uuid.UUID) (err error) {
|
func (paymentService PaymentsService) AddPromotionalCoupon(ctx context.Context, userID uuid.UUID) (err error) {
|
||||||
defer mon.Task()(&ctx, userID)(&err)
|
defer mon.Task()(&ctx, userID)(&err)
|
||||||
|
|
||||||
cards, err := paymentService.ListCreditCards(ctx)
|
if paymentService.service.accounts.PaywallEnabled(userID) {
|
||||||
if err != nil {
|
cards, err := paymentService.ListCreditCards(ctx)
|
||||||
return err
|
if err != nil {
|
||||||
}
|
return err
|
||||||
if len(cards) == 0 {
|
}
|
||||||
return errs.New("user don't have a payment method")
|
if len(cards) == 0 {
|
||||||
|
return errs.New("user don't have a payment method")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return paymentService.service.accounts.Coupons().AddPromotionalCoupon(ctx, userID)
|
return paymentService.service.accounts.Coupons().AddPromotionalCoupon(ctx, userID)
|
||||||
@ -610,6 +616,15 @@ func (s *Service) ActivateAccount(ctx context.Context, activationToken string) (
|
|||||||
return Error.Wrap(err)
|
return Error.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.accounts.PaywallEnabled(user.ID) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.accounts.Coupons().AddPromotionalCoupon(ctx, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Debug(fmt.Sprintf("could not add promotional coupon for user %s", user.ID.String()), zap.Error(Error.Wrap(err)))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -881,22 +896,24 @@ func (s *Service) CreateProject(ctx context.Context, projectInfo ProjectInfo) (p
|
|||||||
return nil, ErrProjLimit.Wrap(err)
|
return nil, ErrProjLimit.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cards, err := s.accounts.CreditCards().List(ctx, auth.User.ID)
|
if s.accounts.PaywallEnabled(auth.User.ID) {
|
||||||
if err != nil {
|
cards, err := s.accounts.CreditCards().List(ctx, auth.User.ID)
|
||||||
s.log.Debug(fmt.Sprintf("could not add promotional coupon for user %s", auth.User.ID.String()), zap.Error(Error.Wrap(err)))
|
if err != nil {
|
||||||
return nil, Error.Wrap(err)
|
s.log.Debug(fmt.Sprintf("could not add promotional coupon for user %s", auth.User.ID.String()), zap.Error(Error.Wrap(err)))
|
||||||
}
|
return nil, Error.Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
balance, err := s.accounts.Balance(ctx, auth.User.ID)
|
balance, err := s.accounts.Balance(ctx, auth.User.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Debug(fmt.Sprintf("could not add promotional coupon for user %s", auth.User.ID.String()), zap.Error(Error.Wrap(err)))
|
s.log.Debug(fmt.Sprintf("could not add promotional coupon for user %s", auth.User.ID.String()), zap.Error(Error.Wrap(err)))
|
||||||
return nil, Error.Wrap(err)
|
return nil, Error.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cards) == 0 && balance.Coins < s.minCoinPayment {
|
if len(cards) == 0 && balance.Coins < s.minCoinPayment {
|
||||||
err = errs.New("no valid payment methods found")
|
err = errs.New("no valid payment methods found")
|
||||||
s.log.Debug(fmt.Sprintf("could not add promotional coupon for user %s", auth.User.ID.String()), zap.Error(Error.Wrap(err)))
|
s.log.Debug(fmt.Sprintf("could not add promotional coupon for user %s", auth.User.ID.String()), zap.Error(Error.Wrap(err)))
|
||||||
return nil, Error.Wrap(err)
|
return nil, Error.Wrap(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.store.WithTx(ctx, func(ctx context.Context, tx DBTx) error {
|
err = s.store.WithTx(ctx, func(ctx context.Context, tx DBTx) error {
|
||||||
@ -1513,3 +1530,9 @@ func findMembershipByProjectID(memberships []ProjectMember, projectID uuid.UUID)
|
|||||||
}
|
}
|
||||||
return ProjectMember{}, false
|
return ProjectMember{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PaywallEnabled returns a true if a credit card or account
|
||||||
|
// balance is required to create projects.
|
||||||
|
func (s *Service) PaywallEnabled(userID uuid.UUID) bool {
|
||||||
|
return s.accounts.PaywallEnabled(userID)
|
||||||
|
}
|
||||||
|
@ -475,7 +475,8 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB,
|
|||||||
pc.CouponValue,
|
pc.CouponValue,
|
||||||
pc.CouponDuration,
|
pc.CouponDuration,
|
||||||
pc.CouponProjectLimit,
|
pc.CouponProjectLimit,
|
||||||
pc.MinCoinPayment)
|
pc.MinCoinPayment,
|
||||||
|
pc.PaywallProportion)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.Combine(err, peer.Close())
|
return nil, errs.Combine(err, peer.Close())
|
||||||
|
@ -46,4 +46,8 @@ type Accounts interface {
|
|||||||
|
|
||||||
// Coupons exposes all needed functionality to manage coupons.
|
// Coupons exposes all needed functionality to manage coupons.
|
||||||
Coupons() Coupons
|
Coupons() Coupons
|
||||||
|
|
||||||
|
// PaywallEnabled returns a true if a credit card or account
|
||||||
|
// balance is required to create projects
|
||||||
|
PaywallEnabled(uuid.UUID) bool
|
||||||
}
|
}
|
||||||
|
@ -24,4 +24,5 @@ type Config struct {
|
|||||||
NodeRepairBandwidthPrice int64 `help:"price node receive for storing TB of repair in cents" default:"1000"`
|
NodeRepairBandwidthPrice int64 `help:"price node receive for storing TB of repair in cents" default:"1000"`
|
||||||
NodeAuditBandwidthPrice int64 `help:"price node receive for storing TB of audit in cents" default:"1000"`
|
NodeAuditBandwidthPrice int64 `help:"price node receive for storing TB of audit in cents" default:"1000"`
|
||||||
NodeDiskSpacePrice int64 `help:"price node receive for storing disk space in cents/TB" default:"150"`
|
NodeDiskSpacePrice int64 `help:"price node receive for storing disk space in cents/TB" default:"150"`
|
||||||
|
PaywallProportion float64 `help:"proportion of users which require a balance to create projects [0-1]" default:"1.0"`
|
||||||
}
|
}
|
||||||
|
@ -194,3 +194,14 @@ func (accounts *accounts) Credits() payments.Credits {
|
|||||||
|
|
||||||
return &credits{service: accounts.service}
|
return &credits{service: accounts.service}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PaywallEnabled returns a true if a credit card or account
|
||||||
|
// balance is required to create projects.
|
||||||
|
func (accounts *accounts) PaywallEnabled(userID uuid.UUID) bool {
|
||||||
|
return BytesAreWithinProportion(userID, accounts.service.PaywallProportion)
|
||||||
|
}
|
||||||
|
|
||||||
|
//BytesAreWithinProportion returns true if first byte is less than the normalized proportion [0..1].
|
||||||
|
func BytesAreWithinProportion(uuidBytes [16]byte, proportion float64) bool {
|
||||||
|
return int(uuidBytes[0]) < int(proportion*256)
|
||||||
|
}
|
||||||
|
39
satellite/payments/stripecoinpayments/accounts_test.go
Normal file
39
satellite/payments/stripecoinpayments/accounts_test.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (C) 2020 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
package stripecoinpayments_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"storj.io/common/uuid"
|
||||||
|
"storj.io/storj/satellite/payments/stripecoinpayments"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBytesAreWithinProportion(t *testing.T) {
|
||||||
|
f := stripecoinpayments.BytesAreWithinProportion
|
||||||
|
assert.False(t, f(uuid.UUID{0}, 0.0))
|
||||||
|
|
||||||
|
assert.False(t, f(uuid.UUID{255}, 0.25))
|
||||||
|
assert.False(t, f(uuid.UUID{192}, 0.25))
|
||||||
|
assert.False(t, f(uuid.UUID{128}, 0.25))
|
||||||
|
assert.False(t, f(uuid.UUID{64}, 0.25))
|
||||||
|
assert.True(t, f(uuid.UUID{63, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, 0.25))
|
||||||
|
assert.True(t, f(uuid.UUID{32}, 0.25))
|
||||||
|
|
||||||
|
assert.False(t, f(uuid.UUID{129}, 0.5))
|
||||||
|
assert.False(t, f(uuid.UUID{128}, 0.5))
|
||||||
|
assert.True(t, f(uuid.UUID{127, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, 0.5))
|
||||||
|
assert.True(t, f(uuid.UUID{127}, 0.5))
|
||||||
|
|
||||||
|
assert.False(t, f(uuid.UUID{255}, 0.75))
|
||||||
|
assert.False(t, f(uuid.UUID{192}, 0.75))
|
||||||
|
assert.True(t, f(uuid.UUID{191, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, 0.75))
|
||||||
|
assert.True(t, f(uuid.UUID{128}, 0.75))
|
||||||
|
assert.True(t, f(uuid.UUID{64}, 0.75))
|
||||||
|
assert.True(t, f(uuid.UUID{32}, 0.75))
|
||||||
|
|
||||||
|
assert.True(t, f(uuid.UUID{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, 1.0))
|
||||||
|
}
|
@ -82,12 +82,13 @@ type Service struct {
|
|||||||
rates coinpayments.CurrencyRateInfos
|
rates coinpayments.CurrencyRateInfos
|
||||||
ratesErr error
|
ratesErr error
|
||||||
|
|
||||||
listingLimit int
|
listingLimit int
|
||||||
nowFn func() time.Time
|
nowFn func() time.Time
|
||||||
|
PaywallProportion float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a Service instance.
|
// NewService creates a Service instance.
|
||||||
func NewService(log *zap.Logger, stripeClient StripeClient, config Config, db DB, projectsDB console.Projects, usageDB accounting.ProjectAccounting, storageTBPrice, egressTBPrice, objectPrice string, bonusRate, couponValue, couponDuration int64, couponProjectLimit memory.Size, minCoinPayment int64) (*Service, error) {
|
func NewService(log *zap.Logger, stripeClient StripeClient, config Config, db DB, projectsDB console.Projects, usageDB accounting.ProjectAccounting, storageTBPrice, egressTBPrice, objectPrice string, bonusRate, couponValue, couponDuration int64, couponProjectLimit memory.Size, minCoinPayment int64, paywallProportion float64) (*Service, error) {
|
||||||
|
|
||||||
coinPaymentsClient := coinpayments.NewClient(
|
coinPaymentsClient := coinpayments.NewClient(
|
||||||
coinpayments.Credentials{
|
coinpayments.Credentials{
|
||||||
@ -132,6 +133,7 @@ func NewService(log *zap.Logger, stripeClient StripeClient, config Config, db DB
|
|||||||
AutoAdvance: config.AutoAdvance,
|
AutoAdvance: config.AutoAdvance,
|
||||||
listingLimit: config.ListingLimit,
|
listingLimit: config.ListingLimit,
|
||||||
nowFn: time.Now,
|
nowFn: time.Now,
|
||||||
|
PaywallProportion: paywallProportion,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,6 +212,10 @@ func (service *Service) updateTransactions(ctx context.Context, ids TransactionA
|
|||||||
|
|
||||||
userID := ids[id]
|
userID := ids[id]
|
||||||
|
|
||||||
|
if !service.Accounts().PaywallEnabled(userID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
rate, err := service.db.Transactions().GetLockedRate(ctx, id)
|
rate, err := service.db.Transactions().GetLockedRate(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
service.log.Error(fmt.Sprintf("could not add promotional coupon for user %s", userID.String()), zap.Error(err))
|
service.log.Error(fmt.Sprintf("could not add promotional coupon for user %s", userID.String()), zap.Error(err))
|
||||||
|
3
scripts/testdata/satellite-config.yaml.lock
vendored
3
scripts/testdata/satellite-config.yaml.lock
vendored
@ -529,6 +529,9 @@ identity.key-path: /root/.local/share/storj/identity/satellite/identity.key
|
|||||||
# price user should pay for each object stored in network per month
|
# price user should pay for each object stored in network per month
|
||||||
# payments.object-price: "0.0000022"
|
# payments.object-price: "0.0000022"
|
||||||
|
|
||||||
|
# proportion of users which require a balance to create projects [0-1]
|
||||||
|
# payments.paywall-proportion: 1
|
||||||
|
|
||||||
# payments provider to use
|
# payments provider to use
|
||||||
# payments.provider: ""
|
# payments.provider: ""
|
||||||
|
|
||||||
|
@ -253,4 +253,25 @@ export class PaymentsHttpApi implements PaymentsApi {
|
|||||||
|
|
||||||
return new TokenDeposit(result.amount, result.address, result.link);
|
return new TokenDeposit(result.amount, result.address, result.link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if paywall is enabled.
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
public async getPaywallEnabledStatus(userId: string): Promise<boolean> {
|
||||||
|
const path = `${this.ROOT_PATH}/paywall-enabled/${userId}`;
|
||||||
|
const response = await this.client.get(path);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
throw new ErrorUnauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('can not process coin payment');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="tour-area">
|
<div class="tour-area">
|
||||||
<div class="tour-area__info-bar" v-show="isInfoBarVisible">
|
<div class="tour-area__info-bar" v-show="isInfoBarVisible && isPaywallEnabled">
|
||||||
<div class="tour-area__info-bar__message">
|
<div class="tour-area__info-bar__message">
|
||||||
<b class="tour-area__info-bar__message__bold">Try Tardigrade with 50 GB Free after adding a payment method.</b>
|
<b class="tour-area__info-bar__message__bold">Try Tardigrade with 50 GB Free after adding a payment method.</b>
|
||||||
<p class="tour-area__info-bar__message__regular"> Cancel before your credit runs out and you’ll never be billed.</p>
|
<p class="tour-area__info-bar__message__regular"> Cancel before your credit runs out and you’ll never be billed.</p>
|
||||||
@ -12,15 +12,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tour-area__content">
|
<div class="tour-area__content">
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
:is-paywall-enabled="isPaywallEnabled"
|
||||||
:is-add-payment-step="isAddPaymentState"
|
:is-add-payment-step="isAddPaymentState"
|
||||||
:is-create-project-step="isCreateProjectState"
|
:is-create-project-step="isCreateProjectState"
|
||||||
:is-create-api-key-step="isCreatApiKeyState"
|
:is-create-api-key-step="isCreatApiKeyState"
|
||||||
:is-upload-data-step="isUploadDataState"
|
:is-upload-data-step="isUploadDataState"
|
||||||
/>
|
/>
|
||||||
<OverviewStep
|
<OverviewStep
|
||||||
v-if="isDefaultState"
|
v-if="isDefaultState && isPaywallEnabled"
|
||||||
@setAddPaymentState="setAddPaymentState"
|
@setAddPaymentState="setAddPaymentState"
|
||||||
/>
|
/>
|
||||||
|
<OverviewStepNoPaywall
|
||||||
|
v-if="isDefaultState && !isPaywallEnabled"
|
||||||
|
@setCreateProjectState="setCreateProjectState"
|
||||||
|
/>
|
||||||
<AddPaymentStep
|
<AddPaymentStep
|
||||||
v-if="isAddPaymentState"
|
v-if="isAddPaymentState"
|
||||||
@setProjectState="setCreateProjectState"
|
@setProjectState="setCreateProjectState"
|
||||||
@ -52,6 +57,7 @@ import AddPaymentStep from '@/components/onboardingTour/steps/AddPaymentStep.vue
|
|||||||
import CreateApiKeyStep from '@/components/onboardingTour/steps/CreateApiKeyStep.vue';
|
import CreateApiKeyStep from '@/components/onboardingTour/steps/CreateApiKeyStep.vue';
|
||||||
import CreateProjectStep from '@/components/onboardingTour/steps/CreateProjectStep.vue';
|
import CreateProjectStep from '@/components/onboardingTour/steps/CreateProjectStep.vue';
|
||||||
import OverviewStep from '@/components/onboardingTour/steps/OverviewStep.vue';
|
import OverviewStep from '@/components/onboardingTour/steps/OverviewStep.vue';
|
||||||
|
import OverviewStepNoPaywall from '@/components/onboardingTour/steps/OverviewStepNoPaywall.vue';
|
||||||
import UploadDataStep from '@/components/onboardingTour/steps/UploadDataStep.vue';
|
import UploadDataStep from '@/components/onboardingTour/steps/UploadDataStep.vue';
|
||||||
|
|
||||||
import CheckedImage from '@/../static/images/common/checked.svg';
|
import CheckedImage from '@/../static/images/common/checked.svg';
|
||||||
@ -59,9 +65,11 @@ import CloseImage from '@/../static/images/onboardingTour/close.svg';
|
|||||||
|
|
||||||
import { RouteConfig } from '@/router';
|
import { RouteConfig } from '@/router';
|
||||||
import { TourState } from '@/utils/constants/onboardingTourEnums';
|
import { TourState } from '@/utils/constants/onboardingTourEnums';
|
||||||
|
import { MetaUtils } from '@/utils/meta';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
OverviewStepNoPaywall,
|
||||||
UploadDataStep,
|
UploadDataStep,
|
||||||
CreateApiKeyStep,
|
CreateApiKeyStep,
|
||||||
CreateProjectStep,
|
CreateProjectStep,
|
||||||
@ -111,6 +119,13 @@ export default class OnboardingTourArea extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if paywall is enabled.
|
||||||
|
*/
|
||||||
|
public get isPaywallEnabled(): boolean {
|
||||||
|
return this.$store.state.paymentsModule.paywallEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if area is in default state.
|
* Indicates if area is in default state.
|
||||||
*/
|
*/
|
||||||
|
@ -5,12 +5,14 @@
|
|||||||
<div class="progress-bar-container">
|
<div class="progress-bar-container">
|
||||||
<div class="progress-bar-container__progress-area">
|
<div class="progress-bar-container__progress-area">
|
||||||
<div
|
<div
|
||||||
|
v-if="isPaywallEnabled"
|
||||||
class="progress-bar-container__progress-area__circle"
|
class="progress-bar-container__progress-area__circle"
|
||||||
:class="{ 'completed-step': isAddPaymentStep || isCreateProjectStep || isCreateApiKeyStep || isUploadDataStep }"
|
:class="{ 'completed-step': isAddPaymentStep || isCreateProjectStep || isCreateApiKeyStep || isUploadDataStep }"
|
||||||
>
|
>
|
||||||
<CheckedImage/>
|
<CheckedImage/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
v-if="isPaywallEnabled"
|
||||||
class="progress-bar-container__progress-area__bar"
|
class="progress-bar-container__progress-area__bar"
|
||||||
:class="{ 'completed-step': isCreateProjectStep || isCreateApiKeyStep || isUploadDataStep }"
|
:class="{ 'completed-step': isCreateProjectStep || isCreateApiKeyStep || isUploadDataStep }"
|
||||||
/>
|
/>
|
||||||
@ -41,8 +43,9 @@
|
|||||||
<CheckedImage/>
|
<CheckedImage/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bar-container__titles-area">
|
<div class="progress-bar-container__titles-area" :class="{ 'titles-area-no-paywall': !isPaywallEnabled }">
|
||||||
<span
|
<span
|
||||||
|
v-if="isPaywallEnabled"
|
||||||
class="progress-bar-container__titles-area__title"
|
class="progress-bar-container__titles-area__title"
|
||||||
:class="{ 'completed-font-color': isAddPaymentStep || isCreateProjectStep || isCreateApiKeyStep || isUploadDataStep }"
|
:class="{ 'completed-font-color': isAddPaymentStep || isCreateProjectStep || isCreateApiKeyStep || isUploadDataStep }"
|
||||||
>
|
>
|
||||||
@ -50,7 +53,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="progress-bar-container__titles-area__title name-your-project-title"
|
class="progress-bar-container__titles-area__title name-your-project-title"
|
||||||
:class="{ 'completed-font-color': isCreateProjectStep || isCreateApiKeyStep || isUploadDataStep }"
|
:class="{ 'completed-font-color': isCreateProjectStep || isCreateApiKeyStep || isUploadDataStep, 'title-no-paywall': !isPaywallEnabled }"
|
||||||
>
|
>
|
||||||
Name Your Project
|
Name Your Project
|
||||||
</span>
|
</span>
|
||||||
@ -82,6 +85,8 @@ import CheckedImage from '@/../static/images/common/checked.svg';
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default class ProgressBar extends Vue {
|
export default class ProgressBar extends Vue {
|
||||||
|
@Prop({ default: false })
|
||||||
|
public readonly isPaywallEnabled: boolean;
|
||||||
@Prop({ default: false })
|
@Prop({ default: false })
|
||||||
public readonly isAddPaymentStep: boolean;
|
public readonly isAddPaymentStep: boolean;
|
||||||
@Prop({ default: false })
|
@Prop({ default: false })
|
||||||
@ -153,6 +158,14 @@ export default class ProgressBar extends Vue {
|
|||||||
color: #2683ff;
|
color: #2683ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.titles-area-no-paywall {
|
||||||
|
padding: 0 188px 0 178px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-no-paywall {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px) {
|
||||||
|
|
||||||
.progress-bar-container {
|
.progress-bar-container {
|
||||||
@ -166,5 +179,9 @@ export default class ProgressBar extends Vue {
|
|||||||
padding: 0 128px 0 128px;
|
padding: 0 128px 0 128px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.titles-area-no-paywall {
|
||||||
|
padding: 0 128px 0 118px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (C) 2020 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
<template src="./overviewStepNoPaywall.html"></template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
|
|
||||||
|
import VButton from '@/components/common/VButton.vue';
|
||||||
|
|
||||||
|
import FirstStepIcon from '@/../static/images/common/one.svg';
|
||||||
|
import ThirdStepIcon from '@/../static/images/common/three.svg';
|
||||||
|
import SecondStepIcon from '@/../static/images/common/two.svg';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
VButton,
|
||||||
|
FirstStepIcon,
|
||||||
|
ThirdStepIcon,
|
||||||
|
SecondStepIcon,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class OverviewStepNoPaywall extends Vue {
|
||||||
|
/**
|
||||||
|
* Holds button click logic.
|
||||||
|
* Sets tour state to adding project state.
|
||||||
|
*/
|
||||||
|
public onClick(): void {
|
||||||
|
this.$emit('setCreateProjectState');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss" src="./overviewStepNoPaywall.scss"></style>
|
@ -0,0 +1,43 @@
|
|||||||
|
<!--Copyright (C) 2020 Storj Labs, Inc.-->
|
||||||
|
<!--See LICENSE for copying information.-->
|
||||||
|
|
||||||
|
<div class="overview-area">
|
||||||
|
<h1 class="overview-area__title">Welcome to Storj</h1>
|
||||||
|
<p class="overview-area__sub-title">
|
||||||
|
You’re just a few steps away from uploading your first object to the 100% secure, decentralized cloud. Simply
|
||||||
|
add a payment method any time before your credit runs out to keep using Storj.
|
||||||
|
</p>
|
||||||
|
<div class="overview-area__steps-area">
|
||||||
|
<div class="overview-area__steps-area__step">
|
||||||
|
<FirstStepIcon class="overview-area__steps-area__step__icon"/>
|
||||||
|
<img class="overview-step-image" src="@/../static/images/onboardingTour/project.jpg" alt="project image">
|
||||||
|
<h2 class="overview-area__steps-area__step__title">Name Your Project</h2>
|
||||||
|
<span class="overview-area__steps-area__step__subtitle">
|
||||||
|
Projects are where buckets are created for storing data.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="overview-area__steps-area__step second-step">
|
||||||
|
<SecondStepIcon class="overview-area__steps-area__step__icon"/>
|
||||||
|
<img class="overview-step-image" src="@/../static/images/onboardingTour/api-key.jpg" alt="api keys image">
|
||||||
|
<h2 class="overview-area__steps-area__step__title">Create an API Key</h2>
|
||||||
|
<span class="overview-area__steps-area__step__subtitle">
|
||||||
|
Generate access to your project to upload data.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="overview-area__steps-area__step">
|
||||||
|
<ThirdStepIcon class="overview-area__steps-area__step__icon"/>
|
||||||
|
<img class="overview-step-image" src="@/../static/images/onboardingTour/uplink.jpg" alt="uplink image">
|
||||||
|
<h2 class="overview-area__steps-area__step__title">Upload Data</h2>
|
||||||
|
<span class="overview-area__steps-area__step__subtitle">
|
||||||
|
Store your data on the secure, decentralized cloud.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<VButton
|
||||||
|
class="get-started-button"
|
||||||
|
label="Get Started"
|
||||||
|
width="251px"
|
||||||
|
height="56px"
|
||||||
|
:on-press="onClick"
|
||||||
|
/>
|
||||||
|
</div>
|
@ -0,0 +1,107 @@
|
|||||||
|
// Copyright (C) 2020 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-area {
|
||||||
|
width: auto;
|
||||||
|
padding: 75px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-family: 'font_regular', sans-serif;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 25px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-family: 'font_bold', sans-serif;
|
||||||
|
font-size: 32px;
|
||||||
|
line-height: 39px;
|
||||||
|
color: #1b2533;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sub-title {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 26px;
|
||||||
|
color: #354049;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 815px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__steps-area {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
|
||||||
|
&__step {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
max-width: 190px;
|
||||||
|
width: 190px;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
position: absolute;
|
||||||
|
top: -15px;
|
||||||
|
left: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 26px;
|
||||||
|
text-align: center;
|
||||||
|
color: #354049;
|
||||||
|
margin: 15px 0 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle {
|
||||||
|
font-family: 'font_regular', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 17px;
|
||||||
|
text-align: center;
|
||||||
|
color: #61666b;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.second-step {
|
||||||
|
margin: 0 50px 0 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1450px) {
|
||||||
|
|
||||||
|
.overview-area {
|
||||||
|
padding: 75px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.second-step {
|
||||||
|
margin: 0 20px 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
|
||||||
|
.overview-area {
|
||||||
|
|
||||||
|
&__steps-area {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&__step {
|
||||||
|
margin: 0 0 30px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -26,6 +26,7 @@ export const PAYMENTS_MUTATIONS = {
|
|||||||
SET_CURRENT_ROLLUP_PRICE: 'SET_CURRENT_ROLLUP_PRICE',
|
SET_CURRENT_ROLLUP_PRICE: 'SET_CURRENT_ROLLUP_PRICE',
|
||||||
SET_PREVIOUS_ROLLUP_PRICE: 'SET_PREVIOUS_ROLLUP_PRICE',
|
SET_PREVIOUS_ROLLUP_PRICE: 'SET_PREVIOUS_ROLLUP_PRICE',
|
||||||
SET_PRICE_SUMMARY: 'SET_PRICE_SUMMARY',
|
SET_PRICE_SUMMARY: 'SET_PRICE_SUMMARY',
|
||||||
|
SET_PAYWALL_ENABLED_STATUS: 'SET_PAYWALL_ENABLED_STATUS',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PAYMENTS_ACTIONS = {
|
export const PAYMENTS_ACTIONS = {
|
||||||
@ -43,6 +44,7 @@ export const PAYMENTS_ACTIONS = {
|
|||||||
GET_PROJECT_USAGE_AND_CHARGES: 'getProjectUsageAndCharges',
|
GET_PROJECT_USAGE_AND_CHARGES: 'getProjectUsageAndCharges',
|
||||||
GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP: 'getProjectUsageAndChargesCurrentRollup',
|
GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP: 'getProjectUsageAndChargesCurrentRollup',
|
||||||
GET_PROJECT_USAGE_AND_CHARGES_PREVIOUS_ROLLUP: 'getProjectUsageAndChargesPreviousRollup',
|
GET_PROJECT_USAGE_AND_CHARGES_PREVIOUS_ROLLUP: 'getProjectUsageAndChargesPreviousRollup',
|
||||||
|
GET_PAYWALL_ENABLED_STATUS: 'getPaywallEnabledStatus',
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -55,6 +57,7 @@ const {
|
|||||||
SET_PAYMENTS_HISTORY,
|
SET_PAYMENTS_HISTORY,
|
||||||
SET_PROJECT_USAGE_AND_CHARGES,
|
SET_PROJECT_USAGE_AND_CHARGES,
|
||||||
SET_PRICE_SUMMARY,
|
SET_PRICE_SUMMARY,
|
||||||
|
SET_PAYWALL_ENABLED_STATUS,
|
||||||
} = PAYMENTS_MUTATIONS;
|
} = PAYMENTS_MUTATIONS;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -71,6 +74,7 @@ const {
|
|||||||
MAKE_TOKEN_DEPOSIT,
|
MAKE_TOKEN_DEPOSIT,
|
||||||
GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP,
|
GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP,
|
||||||
GET_PROJECT_USAGE_AND_CHARGES_PREVIOUS_ROLLUP,
|
GET_PROJECT_USAGE_AND_CHARGES_PREVIOUS_ROLLUP,
|
||||||
|
GET_PAYWALL_ENABLED_STATUS,
|
||||||
} = PAYMENTS_ACTIONS;
|
} = PAYMENTS_ACTIONS;
|
||||||
|
|
||||||
export class PaymentsState {
|
export class PaymentsState {
|
||||||
@ -84,6 +88,7 @@ export class PaymentsState {
|
|||||||
public priceSummary: number = 0;
|
public priceSummary: number = 0;
|
||||||
public startDate: Date = new Date();
|
public startDate: Date = new Date();
|
||||||
public endDate: Date = new Date();
|
public endDate: Date = new Date();
|
||||||
|
public paywallEnabled: boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -148,6 +153,9 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
|
|||||||
|
|
||||||
state.priceSummary = usageItemSummaries.reduce((accumulator, current) => accumulator + current);
|
state.priceSummary = usageItemSummaries.reduce((accumulator, current) => accumulator + current);
|
||||||
},
|
},
|
||||||
|
[SET_PAYWALL_ENABLED_STATUS](state: PaymentsState, paywallEnabledStatus: boolean): void {
|
||||||
|
state.paywallEnabled = paywallEnabledStatus;
|
||||||
|
},
|
||||||
[CLEAR](state: PaymentsState) {
|
[CLEAR](state: PaymentsState) {
|
||||||
state.balance = new AccountBalance();
|
state.balance = new AccountBalance();
|
||||||
state.paymentsHistory = [];
|
state.paymentsHistory = [];
|
||||||
@ -156,6 +164,7 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
|
|||||||
state.creditCards = [];
|
state.creditCards = [];
|
||||||
state.startDate = new Date();
|
state.startDate = new Date();
|
||||||
state.endDate = new Date();
|
state.endDate = new Date();
|
||||||
|
state.paywallEnabled = true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
@ -228,6 +237,11 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
|
|||||||
commit(SET_PROJECT_USAGE_AND_CHARGES, usageAndCharges);
|
commit(SET_PROJECT_USAGE_AND_CHARGES, usageAndCharges);
|
||||||
commit(SET_PRICE_SUMMARY, usageAndCharges);
|
commit(SET_PRICE_SUMMARY, usageAndCharges);
|
||||||
},
|
},
|
||||||
|
[GET_PAYWALL_ENABLED_STATUS]: async function({commit, rootGetters}: any): Promise<void> {
|
||||||
|
const paywallEnabledStatus: boolean = await api.getPaywallEnabledStatus(rootGetters.user.id);
|
||||||
|
|
||||||
|
commit(SET_PAYWALL_ENABLED_STATUS, paywallEnabledStatus);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
canUserCreateFirstProject: (state: PaymentsState): boolean => {
|
canUserCreateFirstProject: (state: PaymentsState): boolean => {
|
||||||
|
@ -69,6 +69,14 @@ export interface PaymentsApi {
|
|||||||
* @throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
makeTokenDeposit(amount: number): Promise<TokenDeposit>;
|
makeTokenDeposit(amount: number): Promise<TokenDeposit>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if paywall is enabled.
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
getPaywallEnabledStatus(userId: string): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AccountBalance {
|
export class AccountBalance {
|
||||||
|
@ -58,6 +58,7 @@ import { AppState } from '@/utils/constants/appStateEnum';
|
|||||||
import { ProjectOwning } from '@/utils/projectOwning';
|
import { ProjectOwning } from '@/utils/projectOwning';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
GET_PAYWALL_ENABLED_STATUS,
|
||||||
SETUP_ACCOUNT,
|
SETUP_ACCOUNT,
|
||||||
GET_BALANCE,
|
GET_BALANCE,
|
||||||
GET_CREDIT_CARDS,
|
GET_CREDIT_CARDS,
|
||||||
@ -100,6 +101,12 @@ export default class DashboardArea extends Vue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch(GET_PAYWALL_ENABLED_STATUS);
|
||||||
|
} catch (error) {
|
||||||
|
await this.$notify.error(`Unable to get paywall enabled status. ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch(SETUP_ACCOUNT);
|
await this.$store.dispatch(SETUP_ACCOUNT);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -49,4 +49,8 @@ export class PaymentsMock implements PaymentsApi {
|
|||||||
makeTokenDeposit(amount: number): Promise<TokenDeposit> {
|
makeTokenDeposit(amount: number): Promise<TokenDeposit> {
|
||||||
return Promise.resolve(new TokenDeposit(amount, 'testAddress', 'testLink'));
|
return Promise.resolve(new TokenDeposit(amount, 'testAddress', 'testLink'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPaywallEnabledStatus(userId: string): Promise<boolean> {
|
||||||
|
throw new Error('Method not implemented');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,22 @@ import ProgressBar from '@/components/onboardingTour/ProgressBar.vue';
|
|||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
describe('ProgressBar.vue', () => {
|
describe('ProgressBar.vue', () => {
|
||||||
it('renders correctly', (): void => {
|
it('renders correctly if paywall is enabled', (): void => {
|
||||||
const wrapper = mount(ProgressBar);
|
const wrapper = mount(ProgressBar, {
|
||||||
|
propsData: {
|
||||||
|
isPaywallEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly if paywall is disabled', (): void => {
|
||||||
|
const wrapper = mount(ProgressBar, {
|
||||||
|
propsData: {
|
||||||
|
isPaywallEnabled: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@ -16,6 +30,7 @@ describe('ProgressBar.vue', () => {
|
|||||||
const wrapper = mount(ProgressBar, {
|
const wrapper = mount(ProgressBar, {
|
||||||
propsData: {
|
propsData: {
|
||||||
isAddPaymentStep: true,
|
isAddPaymentStep: true,
|
||||||
|
isPaywallEnabled: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -27,6 +42,7 @@ describe('ProgressBar.vue', () => {
|
|||||||
const wrapper = mount(ProgressBar, {
|
const wrapper = mount(ProgressBar, {
|
||||||
propsData: {
|
propsData: {
|
||||||
isCreateProjectStep: true,
|
isCreateProjectStep: true,
|
||||||
|
isPaywallEnabled: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -38,6 +54,7 @@ describe('ProgressBar.vue', () => {
|
|||||||
const wrapper = mount(ProgressBar, {
|
const wrapper = mount(ProgressBar, {
|
||||||
propsData: {
|
propsData: {
|
||||||
isCreateApiKeyStep: true,
|
isCreateApiKeyStep: true,
|
||||||
|
isPaywallEnabled: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -49,6 +66,7 @@ describe('ProgressBar.vue', () => {
|
|||||||
const wrapper = mount(ProgressBar, {
|
const wrapper = mount(ProgressBar, {
|
||||||
propsData: {
|
propsData: {
|
||||||
isUploadDataStep: true,
|
isUploadDataStep: true,
|
||||||
|
isPaywallEnabled: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,34 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`ProgressBar.vue renders correctly 1`] = `
|
exports[`ProgressBar.vue renders correctly if paywall is disabled 1`] = `
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar-container__progress-area">
|
||||||
|
<!---->
|
||||||
|
<!---->
|
||||||
|
<div class="progress-bar-container__progress-area__circle"><svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.61854 0.302314C8.02258 -0.100771 8.67764 -0.100771 9.08163 0.302314C9.48569 0.705397 9.48569 1.35893 9.08163 1.76202L4.20463 6.62768C3.8006 7.03077 3.14555 7.03077 2.74152 6.62768L0.303018 4.19485C-0.101006 3.79177 -0.101006 3.13823 0.303018 2.73515C0.707044 2.33206 1.3621 2.33206 1.76612 2.73515L3.47307 4.43813L7.61854 0.302314Z" fill="white"></path>
|
||||||
|
</svg></div>
|
||||||
|
<div class="progress-bar-container__progress-area__bar"></div>
|
||||||
|
<div class="progress-bar-container__progress-area__circle"><svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.61854 0.302314C8.02258 -0.100771 8.67764 -0.100771 9.08163 0.302314C9.48569 0.705397 9.48569 1.35893 9.08163 1.76202L4.20463 6.62768C3.8006 7.03077 3.14555 7.03077 2.74152 6.62768L0.303018 4.19485C-0.101006 3.79177 -0.101006 3.13823 0.303018 2.73515C0.707044 2.33206 1.3621 2.33206 1.76612 2.73515L3.47307 4.43813L7.61854 0.302314Z" fill="white"></path>
|
||||||
|
</svg></div>
|
||||||
|
<div class="progress-bar-container__progress-area__bar"></div>
|
||||||
|
<div class="progress-bar-container__progress-area__circle"><svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.61854 0.302314C8.02258 -0.100771 8.67764 -0.100771 9.08163 0.302314C9.48569 0.705397 9.48569 1.35893 9.08163 1.76202L4.20463 6.62768C3.8006 7.03077 3.14555 7.03077 2.74152 6.62768L0.303018 4.19485C-0.101006 3.79177 -0.101006 3.13823 0.303018 2.73515C0.707044 2.33206 1.3621 2.33206 1.76612 2.73515L3.47307 4.43813L7.61854 0.302314Z" fill="white"></path>
|
||||||
|
</svg></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container__titles-area titles-area-no-paywall">
|
||||||
|
<!----> <span class="progress-bar-container__titles-area__title name-your-project-title title-no-paywall">
|
||||||
|
Name Your Project
|
||||||
|
</span> <span class="progress-bar-container__titles-area__title api-key-title">
|
||||||
|
Create an API Key
|
||||||
|
</span> <span class="progress-bar-container__titles-area__title">
|
||||||
|
Upload Data
|
||||||
|
</span></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`ProgressBar.vue renders correctly if paywall is enabled 1`] = `
|
||||||
<div class="progress-bar-container">
|
<div class="progress-bar-container">
|
||||||
<div class="progress-bar-container__progress-area">
|
<div class="progress-bar-container__progress-area">
|
||||||
<div class="progress-bar-container__progress-area__circle"><svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<div class="progress-bar-container__progress-area__circle"><svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -9,13 +9,14 @@ exports[`OnboardingTourArea.vue renders correctly 1`] = `
|
|||||||
<closeimage-stub class="tour-area__info-bar__close-img"></closeimage-stub>
|
<closeimage-stub class="tour-area__info-bar__close-img"></closeimage-stub>
|
||||||
</div>
|
</div>
|
||||||
<div class="tour-area__content">
|
<div class="tour-area__content">
|
||||||
<progressbar-stub></progressbar-stub>
|
<progressbar-stub ispaywallenabled="true"></progressbar-stub>
|
||||||
<overviewstep-stub></overviewstep-stub>
|
<overviewstep-stub></overviewstep-stub>
|
||||||
<!---->
|
<!---->
|
||||||
<!---->
|
<!---->
|
||||||
<!---->
|
<!---->
|
||||||
<!---->
|
<!---->
|
||||||
<!---->
|
<!---->
|
||||||
|
<!---->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
Loading…
Reference in New Issue
Block a user