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.CouponDuration,
|
||||
pc.CouponProjectLimit,
|
||||
pc.MinCoinPayment)
|
||||
pc.MinCoinPayment,
|
||||
pc.PaywallProportion)
|
||||
}
|
||||
|
||||
// parseBillingPeriodFromString parses provided date string and returns corresponding time.Time.
|
||||
|
@ -483,8 +483,9 @@ func (planet *Planet) newSatellites(count int, satelliteDatabases satellitedbtes
|
||||
ConversionRatesCycleInterval: defaultInterval,
|
||||
ListingLimit: 100,
|
||||
},
|
||||
CouponDuration: 2,
|
||||
CouponValue: 275,
|
||||
CouponDuration: 2,
|
||||
CouponValue: 275,
|
||||
PaywallProportion: 1,
|
||||
},
|
||||
Repairer: repairer.Config{
|
||||
MaxRepair: 10,
|
||||
|
@ -134,7 +134,8 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB,
|
||||
pc.CouponValue,
|
||||
pc.CouponDuration,
|
||||
pc.CouponProjectLimit,
|
||||
pc.MinCoinPayment)
|
||||
pc.MinCoinPayment,
|
||||
pc.PaywallProportion)
|
||||
|
||||
if err != nil {
|
||||
return nil, errs.Combine(err, peer.Close())
|
||||
|
@ -538,7 +538,8 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
|
||||
pc.CouponValue,
|
||||
pc.CouponDuration,
|
||||
pc.CouponProjectLimit,
|
||||
pc.MinCoinPayment)
|
||||
pc.MinCoinPayment,
|
||||
pc.PaywallProportion)
|
||||
|
||||
if err != nil {
|
||||
return nil, errs.Combine(err, peer.Close())
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/zeebo/errs"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
"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.
|
||||
func (p *Payments) serveJSONError(w http.ResponseWriter, status int, err error) {
|
||||
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.CouponDuration,
|
||||
pc.CouponProjectLimit,
|
||||
pc.MinCoinPayment)
|
||||
pc.MinCoinPayment,
|
||||
pc.PaywallProportion)
|
||||
require.NoError(t, err)
|
||||
|
||||
service, err := console.NewService(
|
||||
|
@ -74,7 +74,8 @@ func TestGraphqlQuery(t *testing.T) {
|
||||
pc.CouponValue,
|
||||
pc.CouponDuration,
|
||||
pc.CouponProjectLimit,
|
||||
pc.MinCoinPayment)
|
||||
pc.MinCoinPayment,
|
||||
pc.PaywallProportion)
|
||||
require.NoError(t, err)
|
||||
|
||||
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("/billing-history", paymentController.BillingHistory).Methods(http.MethodGet)
|
||||
paymentsRouter.HandleFunc("/tokens/deposit", paymentController.TokenDeposit).Methods(http.MethodPost)
|
||||
paymentsRouter.HandleFunc("/paywall-enabled/{userId}", paymentController.PaywallEnabled).Methods(http.MethodGet)
|
||||
|
||||
if server.config.StaticDir != "" {
|
||||
router.HandleFunc("/activation/", server.accountActivationHandler)
|
||||
|
@ -174,9 +174,13 @@ func (paymentService PaymentsService) AddCreditCard(ctx context.Context, creditC
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
if !paymentService.service.accounts.PaywallEnabled(auth.User.ID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = paymentService.AddPromotionalCoupon(ctx, auth.User.ID)
|
||||
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
|
||||
@ -387,12 +391,14 @@ func (paymentService PaymentsService) PopulatePromotionalCoupons(ctx context.Con
|
||||
func (paymentService PaymentsService) AddPromotionalCoupon(ctx context.Context, userID uuid.UUID) (err error) {
|
||||
defer mon.Task()(&ctx, userID)(&err)
|
||||
|
||||
cards, err := paymentService.ListCreditCards(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(cards) == 0 {
|
||||
return errs.New("user don't have a payment method")
|
||||
if paymentService.service.accounts.PaywallEnabled(userID) {
|
||||
cards, err := paymentService.ListCreditCards(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(cards) == 0 {
|
||||
return errs.New("user don't have a payment method")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -881,22 +896,24 @@ func (s *Service) CreateProject(ctx context.Context, projectInfo ProjectInfo) (p
|
||||
return nil, ErrProjLimit.Wrap(err)
|
||||
}
|
||||
|
||||
cards, err := s.accounts.CreditCards().List(ctx, auth.User.ID)
|
||||
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)))
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
if s.accounts.PaywallEnabled(auth.User.ID) {
|
||||
cards, err := s.accounts.CreditCards().List(ctx, auth.User.ID)
|
||||
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)))
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
balance, err := s.accounts.Balance(ctx, auth.User.ID)
|
||||
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)))
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
balance, err := s.accounts.Balance(ctx, auth.User.ID)
|
||||
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)))
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
if len(cards) == 0 && balance.Coins < s.minCoinPayment {
|
||||
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)))
|
||||
return nil, Error.Wrap(err)
|
||||
if len(cards) == 0 && balance.Coins < s.minCoinPayment {
|
||||
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)))
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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.CouponDuration,
|
||||
pc.CouponProjectLimit,
|
||||
pc.MinCoinPayment)
|
||||
pc.MinCoinPayment,
|
||||
pc.PaywallProportion)
|
||||
|
||||
if err != nil {
|
||||
return nil, errs.Combine(err, peer.Close())
|
||||
|
@ -46,4 +46,8 @@ type Accounts interface {
|
||||
|
||||
// Coupons exposes all needed functionality to manage 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"`
|
||||
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"`
|
||||
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}
|
||||
}
|
||||
|
||||
// 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
|
||||
ratesErr error
|
||||
|
||||
listingLimit int
|
||||
nowFn func() time.Time
|
||||
listingLimit int
|
||||
nowFn func() time.Time
|
||||
PaywallProportion float64
|
||||
}
|
||||
|
||||
// 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(
|
||||
coinpayments.Credentials{
|
||||
@ -132,6 +133,7 @@ func NewService(log *zap.Logger, stripeClient StripeClient, config Config, db DB
|
||||
AutoAdvance: config.AutoAdvance,
|
||||
listingLimit: config.ListingLimit,
|
||||
nowFn: time.Now,
|
||||
PaywallProportion: paywallProportion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -210,6 +212,10 @@ func (service *Service) updateTransactions(ctx context.Context, ids TransactionA
|
||||
|
||||
userID := ids[id]
|
||||
|
||||
if !service.Accounts().PaywallEnabled(userID) {
|
||||
continue
|
||||
}
|
||||
|
||||
rate, err := service.db.Transactions().GetLockedRate(ctx, id)
|
||||
if err != nil {
|
||||
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
|
||||
# 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: ""
|
||||
|
||||
|
@ -253,4 +253,25 @@ export class PaymentsHttpApi implements PaymentsApi {
|
||||
|
||||
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>
|
||||
<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">
|
||||
<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>
|
||||
@ -12,15 +12,20 @@
|
||||
</div>
|
||||
<div class="tour-area__content">
|
||||
<ProgressBar
|
||||
:is-paywall-enabled="isPaywallEnabled"
|
||||
:is-add-payment-step="isAddPaymentState"
|
||||
:is-create-project-step="isCreateProjectState"
|
||||
:is-create-api-key-step="isCreatApiKeyState"
|
||||
:is-upload-data-step="isUploadDataState"
|
||||
/>
|
||||
<OverviewStep
|
||||
v-if="isDefaultState"
|
||||
v-if="isDefaultState && isPaywallEnabled"
|
||||
@setAddPaymentState="setAddPaymentState"
|
||||
/>
|
||||
<OverviewStepNoPaywall
|
||||
v-if="isDefaultState && !isPaywallEnabled"
|
||||
@setCreateProjectState="setCreateProjectState"
|
||||
/>
|
||||
<AddPaymentStep
|
||||
v-if="isAddPaymentState"
|
||||
@setProjectState="setCreateProjectState"
|
||||
@ -52,6 +57,7 @@ import AddPaymentStep from '@/components/onboardingTour/steps/AddPaymentStep.vue
|
||||
import CreateApiKeyStep from '@/components/onboardingTour/steps/CreateApiKeyStep.vue';
|
||||
import CreateProjectStep from '@/components/onboardingTour/steps/CreateProjectStep.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 CheckedImage from '@/../static/images/common/checked.svg';
|
||||
@ -59,9 +65,11 @@ import CloseImage from '@/../static/images/onboardingTour/close.svg';
|
||||
|
||||
import { RouteConfig } from '@/router';
|
||||
import { TourState } from '@/utils/constants/onboardingTourEnums';
|
||||
import { MetaUtils } from '@/utils/meta';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
OverviewStepNoPaywall,
|
||||
UploadDataStep,
|
||||
CreateApiKeyStep,
|
||||
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.
|
||||
*/
|
||||
|
@ -5,12 +5,14 @@
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar-container__progress-area">
|
||||
<div
|
||||
v-if="isPaywallEnabled"
|
||||
class="progress-bar-container__progress-area__circle"
|
||||
:class="{ 'completed-step': isAddPaymentStep || isCreateProjectStep || isCreateApiKeyStep || isUploadDataStep }"
|
||||
>
|
||||
<CheckedImage/>
|
||||
</div>
|
||||
<div
|
||||
v-if="isPaywallEnabled"
|
||||
class="progress-bar-container__progress-area__bar"
|
||||
:class="{ 'completed-step': isCreateProjectStep || isCreateApiKeyStep || isUploadDataStep }"
|
||||
/>
|
||||
@ -41,8 +43,9 @@
|
||||
<CheckedImage/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-bar-container__titles-area">
|
||||
<div class="progress-bar-container__titles-area" :class="{ 'titles-area-no-paywall': !isPaywallEnabled }">
|
||||
<span
|
||||
v-if="isPaywallEnabled"
|
||||
class="progress-bar-container__titles-area__title"
|
||||
:class="{ 'completed-font-color': isAddPaymentStep || isCreateProjectStep || isCreateApiKeyStep || isUploadDataStep }"
|
||||
>
|
||||
@ -50,7 +53,7 @@
|
||||
</span>
|
||||
<span
|
||||
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
|
||||
</span>
|
||||
@ -82,6 +85,8 @@ import CheckedImage from '@/../static/images/common/checked.svg';
|
||||
})
|
||||
|
||||
export default class ProgressBar extends Vue {
|
||||
@Prop({ default: false })
|
||||
public readonly isPaywallEnabled: boolean;
|
||||
@Prop({ default: false })
|
||||
public readonly isAddPaymentStep: boolean;
|
||||
@Prop({ default: false })
|
||||
@ -153,6 +158,14 @@ export default class ProgressBar extends Vue {
|
||||
color: #2683ff;
|
||||
}
|
||||
|
||||
.titles-area-no-paywall {
|
||||
padding: 0 188px 0 178px;
|
||||
}
|
||||
|
||||
.title-no-paywall {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
|
||||
.progress-bar-container {
|
||||
@ -166,5 +179,9 @@ export default class ProgressBar extends Vue {
|
||||
padding: 0 128px 0 128px;
|
||||
}
|
||||
}
|
||||
|
||||
.titles-area-no-paywall {
|
||||
padding: 0 128px 0 118px;
|
||||
}
|
||||
}
|
||||
</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_PREVIOUS_ROLLUP_PRICE: 'SET_PREVIOUS_ROLLUP_PRICE',
|
||||
SET_PRICE_SUMMARY: 'SET_PRICE_SUMMARY',
|
||||
SET_PAYWALL_ENABLED_STATUS: 'SET_PAYWALL_ENABLED_STATUS',
|
||||
};
|
||||
|
||||
export const PAYMENTS_ACTIONS = {
|
||||
@ -43,6 +44,7 @@ export const PAYMENTS_ACTIONS = {
|
||||
GET_PROJECT_USAGE_AND_CHARGES: 'getProjectUsageAndCharges',
|
||||
GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP: 'getProjectUsageAndChargesCurrentRollup',
|
||||
GET_PROJECT_USAGE_AND_CHARGES_PREVIOUS_ROLLUP: 'getProjectUsageAndChargesPreviousRollup',
|
||||
GET_PAYWALL_ENABLED_STATUS: 'getPaywallEnabledStatus',
|
||||
};
|
||||
|
||||
const {
|
||||
@ -55,6 +57,7 @@ const {
|
||||
SET_PAYMENTS_HISTORY,
|
||||
SET_PROJECT_USAGE_AND_CHARGES,
|
||||
SET_PRICE_SUMMARY,
|
||||
SET_PAYWALL_ENABLED_STATUS,
|
||||
} = PAYMENTS_MUTATIONS;
|
||||
|
||||
const {
|
||||
@ -71,6 +74,7 @@ const {
|
||||
MAKE_TOKEN_DEPOSIT,
|
||||
GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP,
|
||||
GET_PROJECT_USAGE_AND_CHARGES_PREVIOUS_ROLLUP,
|
||||
GET_PAYWALL_ENABLED_STATUS,
|
||||
} = PAYMENTS_ACTIONS;
|
||||
|
||||
export class PaymentsState {
|
||||
@ -84,6 +88,7 @@ export class PaymentsState {
|
||||
public priceSummary: number = 0;
|
||||
public startDate: 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);
|
||||
},
|
||||
[SET_PAYWALL_ENABLED_STATUS](state: PaymentsState, paywallEnabledStatus: boolean): void {
|
||||
state.paywallEnabled = paywallEnabledStatus;
|
||||
},
|
||||
[CLEAR](state: PaymentsState) {
|
||||
state.balance = new AccountBalance();
|
||||
state.paymentsHistory = [];
|
||||
@ -156,6 +164,7 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
|
||||
state.creditCards = [];
|
||||
state.startDate = new Date();
|
||||
state.endDate = new Date();
|
||||
state.paywallEnabled = true;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
@ -228,6 +237,11 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
|
||||
commit(SET_PROJECT_USAGE_AND_CHARGES, 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: {
|
||||
canUserCreateFirstProject: (state: PaymentsState): boolean => {
|
||||
|
@ -69,6 +69,14 @@ export interface PaymentsApi {
|
||||
* @throws Error
|
||||
*/
|
||||
makeTokenDeposit(amount: number): Promise<TokenDeposit>;
|
||||
|
||||
/**
|
||||
* Indicates if paywall is enabled.
|
||||
*
|
||||
* @param userId
|
||||
* @throws Error
|
||||
*/
|
||||
getPaywallEnabledStatus(userId: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export class AccountBalance {
|
||||
|
@ -58,6 +58,7 @@ import { AppState } from '@/utils/constants/appStateEnum';
|
||||
import { ProjectOwning } from '@/utils/projectOwning';
|
||||
|
||||
const {
|
||||
GET_PAYWALL_ENABLED_STATUS,
|
||||
SETUP_ACCOUNT,
|
||||
GET_BALANCE,
|
||||
GET_CREDIT_CARDS,
|
||||
@ -100,6 +101,12 @@ export default class DashboardArea extends Vue {
|
||||
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 {
|
||||
await this.$store.dispatch(SETUP_ACCOUNT);
|
||||
} catch (error) {
|
||||
|
@ -49,4 +49,8 @@ export class PaymentsMock implements PaymentsApi {
|
||||
makeTokenDeposit(amount: number): Promise<TokenDeposit> {
|
||||
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';
|
||||
|
||||
describe('ProgressBar.vue', () => {
|
||||
it('renders correctly', (): void => {
|
||||
const wrapper = mount(ProgressBar);
|
||||
it('renders correctly if paywall is enabled', (): void => {
|
||||
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();
|
||||
});
|
||||
@ -16,6 +30,7 @@ describe('ProgressBar.vue', () => {
|
||||
const wrapper = mount(ProgressBar, {
|
||||
propsData: {
|
||||
isAddPaymentStep: true,
|
||||
isPaywallEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -27,6 +42,7 @@ describe('ProgressBar.vue', () => {
|
||||
const wrapper = mount(ProgressBar, {
|
||||
propsData: {
|
||||
isCreateProjectStep: true,
|
||||
isPaywallEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -38,6 +54,7 @@ describe('ProgressBar.vue', () => {
|
||||
const wrapper = mount(ProgressBar, {
|
||||
propsData: {
|
||||
isCreateApiKeyStep: true,
|
||||
isPaywallEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -49,6 +66,7 @@ describe('ProgressBar.vue', () => {
|
||||
const wrapper = mount(ProgressBar, {
|
||||
propsData: {
|
||||
isUploadDataStep: true,
|
||||
isPaywallEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,34 @@
|
||||
// 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__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">
|
||||
|
@ -9,13 +9,14 @@ exports[`OnboardingTourArea.vue renders correctly 1`] = `
|
||||
<closeimage-stub class="tour-area__info-bar__close-img"></closeimage-stub>
|
||||
</div>
|
||||
<div class="tour-area__content">
|
||||
<progressbar-stub></progressbar-stub>
|
||||
<progressbar-stub ispaywallenabled="true"></progressbar-stub>
|
||||
<overviewstep-stub></overviewstep-stub>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
Loading…
Reference in New Issue
Block a user