Commit a178c768 authored by Emiliano Balbuena's avatar Emiliano Balbuena

(wip): Wire Payment component redux; Pro subscription component

1 merge request!578WIP: Marketing pages
Pipeline #88069415 failed with stages
in 6 minutes and 16 seconds
......@@ -35,6 +35,8 @@
display: block;
font-size: 15px;
padding: 12px 15px;
width: 100%;
box-sizing: border-box;
}
&.mf-button--always-inline {
......
export type Currency = 'usd' | 'tokens';
export type Currency = 'tokens' | 'usd';
export const tokenRate = 4;
// TODO: Move to an async service
export default function currency(tokens: number, currency: Currency): string {
switch (currency) {
case 'usd':
return `$${(tokens / tokenRate).toLocaleString()}`;
export default function currency(value: number, type: Currency) {
switch (type) {
case 'tokens':
return `${tokens.toLocaleString()} tokens`;
return `${value.toLocaleString()} tokens`;
case 'usd':
return `$ ${value.toLocaleString()}`;
}
}
......@@ -76,6 +76,7 @@ export interface MindsUser {
subscribed?: boolean;
rating?: number;
eth_wallet?: string;
is_admin?: boolean;
is_mature?: boolean;
mature_lock?: boolean;
tags?: Array<string>;
......
......@@ -14,8 +14,9 @@ import { WireService } from '../wire/wire.service';
import { WireStruc } from '../wire/creator/creator.component';
import { OverlayModalService } from '../../services/ux/overlay-modal';
import { SignupModalService } from '../modals/signup/service';
import { WirePaymentsCreatorComponent } from '../wire/payments-creator/creator.component';
import { Session } from '../../services/session';
import { WirePaymentsCreatorComponent } from '../wire/creator/payments/payments.creator.component';
import { WirePaymentHandlersService } from '../wire/wire-payment-handlers.service';
@Component({
selector: 'm-plus--subscription',
......@@ -34,6 +35,7 @@ export class PlusSubscriptionComponent {
@Input('showSubscription') showSubscription: boolean;
payment: any = {};
payload: any;
minds = window.Minds;
constructor(
private client: Client,
......@@ -42,6 +44,7 @@ export class PlusSubscriptionComponent {
private web3Wallet: Web3WalletService,
private overlayModal: OverlayModalService,
private modal: SignupModalService,
private wirePaymentHandlers: WirePaymentHandlersService,
public session: Session,
private cd: ChangeDetectorRef
) {}
......@@ -74,15 +77,9 @@ export class PlusSubscriptionComponent {
return;
}
this.payment.period = period;
this.payment.amount = amount;
this.payment.recurring = true;
this.payment.entity_guid = '730071191229833224';
this.payment.receiver = this.blockchain.plus_address;
const creator = this.overlayModal.create(
WirePaymentsCreatorComponent,
this.payment,
await this.wirePaymentHandlers.get('plus'),
{
default: this.payment,
onComplete: wire => {
......
<div *ngIf="isLoggedIn">
<ng-container *ngIf="!inProgress; else inProgressSpinner">
<button
*ngIf="!active"
class="mf-button mf-button--alt"
[disabled]="inProgress || criticalError"
(click)="enable()"
>
Upgrade to Pro
</button>
<div>
<div class="m-proSubscriptionPlan__toggleContainer">
<div class="m-proSubscriptionPlan__toggle">
<span>Yearly</span>
<span
><m-toggle
[(mModel)]="interval"
leftValue="yearly"
rightValue="monthly"
></m-toggle
></span>
<span>Monthly</span>
</div>
<div class="m-proSubscriptionPlan__toggle">
<span>USD</span>
<span
><m-toggle
[(mModel)]="currency"
leftValue="usd"
rightValue="tokens"
></m-toggle
></span>
<span>Tokens</span>
</div>
</div>
<div class="m-proSubscriptionPlan__pricing">
<span class="m-proSubscriptionPlanPricing__amount">
<span>{{ pricing.amount }}</span> per month
</span>
<span
class="m-proSubscriptionPlanPricing__offer"
*ngIf="pricing.offerFrom"
>
{{ pricing.offerFrom }} per month
</span>
</div>
<div>
<button
*ngIf="!active"
class="mf-button mf-button--alt"
[disabled]="inProgress || criticalError"
(click)="enable()"
>
Upgrade to Pro
</button>
</div>
</div>
<button
*ngIf="active"
......
......@@ -8,9 +8,14 @@ import {
} from '@angular/core';
import { Session } from '../../../services/session';
import { ProService } from '../pro.service';
import { WireCreatorComponent } from '../../wire/creator/creator.component';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { EntitiesService } from '../../../common/services/entities.service';
import { WirePaymentsCreatorComponent } from '../../wire/creator/payments/payments.creator.component';
import { WirePaymentHandlersService } from '../../wire/wire-payment-handlers.service';
import {
UpgradeOptionCurrency,
UpgradeOptionInterval,
} from '../../upgrades/upgrade-options.component';
import currency from '../../../helpers/currency';
@Component({
selector: 'm-pro--subscription',
......@@ -22,6 +27,10 @@ export class ProSubscriptionComponent implements OnInit {
@Output() onDisable: EventEmitter<any> = new EventEmitter();
interval: UpgradeOptionInterval = 'yearly';
currency: UpgradeOptionCurrency = 'tokens';
isLoggedIn: boolean = false;
inProgress: boolean = false;
......@@ -38,7 +47,7 @@ export class ProSubscriptionComponent implements OnInit {
protected service: ProService,
protected session: Session,
protected overlayModal: OverlayModalService,
protected entities: EntitiesService,
protected wirePaymentHandlers: WirePaymentHandlersService,
protected cd: ChangeDetectorRef
) {}
......@@ -72,20 +81,20 @@ export class ProSubscriptionComponent implements OnInit {
this.detectChanges();
try {
const proHandler = ((await this.entities
.setCastToActivities(false)
.fetch([`urn:user:${this.minds.handlers.pro}`])) as any).entities[0];
this.overlayModal
.create(WireCreatorComponent, proHandler, {
onComplete: () => {
this.active = true;
this.minds.user.pro = true;
this.onEnable.emit(Date.now());
this.inProgress = false;
this.detectChanges();
},
})
.create(
WirePaymentsCreatorComponent,
await this.wirePaymentHandlers.get('pro'),
{
onComplete: () => {
this.active = true;
this.minds.user.pro = true;
this.onEnable.emit(Date.now());
this.inProgress = false;
this.detectChanges();
},
}
)
.onDidDismiss(() => {
this.inProgress = false;
this.detectChanges();
......@@ -121,6 +130,29 @@ export class ProSubscriptionComponent implements OnInit {
this.detectChanges();
}
get pricing() {
if (this.interval === 'yearly') {
return {
amount: currency(
this.minds.upgrades.pro.yearly[this.currency] / 12,
this.currency
),
offerFrom: currency(
this.minds.upgrades.pro.monthly[this.currency],
this.currency
),
};
} else if (this.interval === 'monthly') {
return {
amount: currency(
this.minds.upgrades.pro.monthly[this.currency],
this.currency
),
offerFrom: null,
};
}
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
......
@import '../../../foundation/grid-values';
m-pro--subscription {
.m-proSubscriptionPlan__toggleContainer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
margin-bottom: 23px;
@media screen and (max-width: $m-grid-min-vp) {
justify-content: center;
margin-bottom: 40px;
}
}
.m-proSubscriptionPlan__toggle {
display: flex;
flex-direction: row;
align-items: center;
justify-content: stretch;
margin-right: 50px;
&:last-child {
margin-right: 0;
}
}
.m-proSubscriptionPlan__pricing {
margin-bottom: 24px;
@media screen and (max-width: $m-grid-min-vp) {
margin-bottom: 40px;
text-align: center;
}
.m-proSubscriptionPlanPricing__amount {
display: inline-block;
font-size: 18px;
font-weight: bold;
line-height: 24px;
@include m-theme() {
color: themed($m-grey-600);
}
@media screen and (max-width: $m-grid-min-vp) {
display: block;
}
span {
font-size: 24px;
@include m-theme() {
color: themed($m-black);
}
}
}
.m-proSubscriptionPlanPricing__offer {
display: inline-block;
font-size: 14px;
text-decoration: line-through;
margin-left: 10px;
@include m-theme() {
color: themed($m-red);
}
@media screen and (max-width: $m-grid-min-vp) {
display: block;
margin-left: 0;
}
}
}
.m-proSubscription__error {
display: block;
font-weight: bold;
color: #fff;
margin: 8px 0 0 5px;
@include m-theme() {
color: themed($m-red);
}
}
}
......@@ -55,7 +55,8 @@
<p>
<a
class="m-upgradesUpgradeOptionsPlan__moreInfo"
[routerLink]="plusRouterLink"
routerLink="/plus"
[queryParams]="intervalCurrencyQueryParams"
>
More info
</a>
......@@ -78,7 +79,8 @@
<div class="m-upgradesUpgradeOptionsPlan__row">
<a
class="mf-button m-upgradesUpgradeOptionsPlan__action"
[routerLink]="plusRouterLink"
routerLink="/plus"
[queryParams]="intervalCurrencyQueryParams"
>
Upgrade to Plus
</a>
......@@ -110,7 +112,8 @@
<p>
<a
class="m-upgradesUpgradeOptionsPlan__moreInfo"
[routerLink]="proRouterLink"
routerLink="/pro"
[queryParams]="intervalCurrencyQueryParams"
>
More info
</a>
......@@ -133,7 +136,8 @@
<div class="m-upgradesUpgradeOptionsPlan__row">
<a
class="mf-button m-upgradesUpgradeOptionsPlan__action"
[routerLink]="proRouterLink"
routerLink="/pro"
[queryParams]="intervalCurrencyQueryParams"
>
Upgrade to Pro
</a>
......
@import '../../foundation/grid-values';
m-upgrades__upgradeOptions {
display: block;
}
......
import { ChangeDetectionStrategy, Component } from '@angular/core';
import currency, { Currency } from '../../helpers/currency';
type UpgradeOptionInterval = 'yearly' | 'monthly';
export type UpgradeOptionInterval = 'yearly' | 'monthly';
type UpgradeOptionCurrency = Currency;
export type UpgradeOptionCurrency = Currency;
@Component({
selector: 'm-upgrades__upgradeOptions',
......@@ -11,27 +11,34 @@ type UpgradeOptionCurrency = Currency;
templateUrl: 'upgrade-options.component.html',
})
export class UpgradeOptionsComponent {
minds = window.Minds;
interval: UpgradeOptionInterval = 'yearly';
currency: UpgradeOptionCurrency = 'tokens';
get plusRouterLink() {
return ['/plus', { i: this.interval, c: this.currency }];
}
get proRouterLink() {
return ['/pro', { i: this.interval, c: this.currency }];
get intervalCurrencyQueryParams() {
return { i: this.interval, c: this.currency };
}
get plusPricing() {
if (this.interval === 'yearly') {
return {
amount: currency(20, this.currency),
offerFrom: currency(28, this.currency),
amount: currency(
this.minds.upgrades.plus.yearly[this.currency] / 12,
this.currency
),
offerFrom: currency(
this.minds.upgrades.plus.monthly[this.currency],
this.currency
),
};
} else if (this.interval === 'monthly') {
return {
amount: currency(28, this.currency),
amount: currency(
this.minds.upgrades.plus.monthly[this.currency],
this.currency
),
offerFrom: null,
};
}
......@@ -40,12 +47,21 @@ export class UpgradeOptionsComponent {
get proPricing() {
if (this.interval === 'yearly') {
return {
amount: currency(200, this.currency),
offerFrom: currency(240, this.currency),
amount: currency(
this.minds.upgrades.pro.yearly[this.currency] / 12,
this.currency
),
offerFrom: currency(
this.minds.upgrades.pro.monthly[this.currency],
this.currency
),
};
} else if (this.interval === 'monthly') {
return {
amount: currency(240, this.currency),
amount: currency(
this.minds.upgrades.pro.monthly[this.currency],
this.currency
),
offerFrom: null,
};
}
......
......@@ -162,6 +162,11 @@
outline: none;
}
}
.m-wire--creator-wide-input--fixedAmount {
margin-right: 0.35em;
font-weight: 700;
}
}
.m-wire--creator-selector {
......
m-wire__paymentsCreator {
.m-wire--creator .m-wire--creator--header h2 {
font-weight: bold;
}
.m-wire--creator .m-wire--creator--recurring {
margin-top: 35px;
}
}
import { Component } from '@angular/core';
import { WireCreatorComponent } from '../creator.component';
import { CurrencyPipe } from '@angular/common';
@Component({
providers: [CurrencyPipe],
selector: 'm-wire__paymentsCreator',
templateUrl: 'payments.creator.component.html',
})
export class WirePaymentsCreatorComponent extends WireCreatorComponent {}
import {
Component,
Input,
ViewChild,
ElementRef,
ChangeDetectorRef,
} from '@angular/core';
import { CurrencyPipe } from '@angular/common';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { Client } from '../../../services/api';
import { Session } from '../../../services/session';
import { WireService } from '../wire.service';
import { Web3WalletService } from '../../blockchain/web3-wallet.service';
import { TokenContractService } from '../../blockchain/contracts/token-contract.service';
import { GetMetamaskComponent } from '../../blockchain/metamask/getmetamask.component';
import { MindsUser } from '../../../interfaces/entities';
import { Router } from '@angular/router';
export type PayloadType = 'onchain' | 'offchain';
export class VisibleWireError extends Error {
visible: boolean = true;
}
export interface WireStruc {
amount: number | '';
payloadType: PayloadType | null;
guid: any;
recurring: boolean;
payload: any;
period: string;
}
@Component({
providers: [CurrencyPipe],
selector: 'm-wire-payments--creator',
templateUrl: 'creator.component.html',
})
export class WirePaymentsCreatorComponent {
minds = window.Minds;
blockchain = window.Minds.blockchain;
@Input() receiver;
wire: WireStruc = {
amount: 1,
payloadType: 'onchain',
guid: null,
recurring: false,
payload: null,
period: null,
};
owner: any;
tokens: number;
sums: any;
rates = {
balance: null,
rate: 1,
min: 250,
cap: 5000,
usd: 1,
tokens: 1,
};
editingAmount: boolean = false;
initialized: boolean = false;
inProgress: boolean = false;
success: boolean = false;
criticalError: boolean = false;
error: string = '';
tokenRate: number;
defaultAmount: number | '' = this.wire.amount;
protected submitted: boolean;
@Input('payment') set data(payment) {
this.wire.amount = payment.amount;
this.wire.period = payment.period;
this.wire.recurring = true;
this.wire.guid = payment.entity_guid;
this.receiver = payment.receiver;
}
_opts: any;
set opts(opts: any) {
this._opts = opts;
this.setDefaults();
}
balances = {
onchain: null,
offchain: null,
onChainAddress: '',
isReceiverOnchain: false,
wireCap: null,
};
constructor(
public session: Session,
private wireService: WireService,
private cd: ChangeDetectorRef,
private overlayModal: OverlayModalService,
private client: Client,
private currency: CurrencyPipe,
private web3Wallet: Web3WalletService,
private tokenContract: TokenContractService,
private router: Router
) {}
ngOnInit() {
this.load().then(() => {
this.initialized = true;
});
this.loadBalances();
this.loadTokenRate();
}
async loadBalances() {
try {
let currentWallet = await this.web3Wallet.getCurrentWallet();
if (currentWallet) {
this.loadCurrentWalletBalance(currentWallet);
}
let response: any = await this.client.get(
`api/v2/blockchain/wallet/balance`
);
if (!response) {
return;
}
this.balances.wireCap = response.wireCap;
this.balances.offchain = response.addresses[1].balance;
if (!currentWallet) {
this.balances.onchain = response.addresses[0].balance;
this.balances.onChainAddress = response.addresses[0].address;
this.balances.isReceiverOnchain = true;
}
} catch (e) {
console.error(e);
}
}
async loadCurrentWalletBalance(address) {
try {
this.balances.onChainAddress = address;
this.balances.isReceiverOnchain = false;
const balance = await this.tokenContract.balanceOf(address);
this.balances.onchain = balance[0].toString();
} catch (e) {
console.error(e);
}
}
async loadTokenRate() {
let response: any = await this.client.get(`api/v2/blockchain/rate/tokens`);
if (response && response.rate) {
this.tokenRate = response.rate;
}
}
// Load settings
/**
* Loads wire settings from server (using Boost rates)
*/
load() {
this.inProgress = true;
return this.client
.get(`api/v2/boost/rates`)
.then((rates: any) => {
this.inProgress = false;
this.rates = rates;
})
.catch(e => {
this.inProgress = false;
this.criticalError = true;
this.error = 'Internal server error';
});
}
setDefaults() {
this.wire.recurring = true;
let payloadType = localStorage.getItem('preferred-payment-method');
if (['onchain', 'offchain'].indexOf(payloadType) === -1) {
payloadType = 'offchain';
}
this.setPayloadType(<PayloadType>payloadType);
}
// General
/**
* Sets the wire currency
*/
setPayloadType(payloadType: PayloadType | null) {
this.wire.payloadType = payloadType;
this.wire.payload = null;
if (payloadType === 'onchain') {
this.setOnchainNoncePayload('');
}
localStorage.setItem('preferred-payment-method', payloadType);
this.roundAmount();
this.showErrors();
}
/**
* Sets the wire payment nonce
*/
setNoncePayload(nonce: any) {
this.wire.payload = nonce;
this.showErrors();
}
/**
* Sets the onchain specific wire payment nonce
*/
setOnchainNoncePayload(address: string) {
return this.setNoncePayload({ receiver: this.receiver, address });
}
/**
* Round by 4
*/
roundAmount() {
this.wire.amount =
Math.round(parseFloat(`${this.wire.amount}`) * 10000) / 10000;
}
// Charge and rates
/**
* Calculates base charges (any other % based fee)
*/
calcBaseCharges(type: string): number {
// NOTE: Can be used to calculate fees
return <number>this.wire.amount;
}
/**
* Calculate charges including priority
*/
calcCharges(type: string): number {
// NOTE: Can be used to calculate bonuses
return this.calcBaseCharges(type);
}
// Rate preview
getTokenAmountRate(amount) {
if (!this.tokenRate) {
return 0;
}
return amount * this.tokenRate;
}
/**
* Toggles the recurring subscription based on its current status
*/
toggleRecurring() {
this.wire.recurring = !this.wire.recurring;
this.showErrors();
}
/**
* Validates if the wire payment can be submitted using the current settings
*/
validate() {
if (this.wire.amount <= 0) {
throw new Error('Amount should be greater than zero.');
}
if (!this.wire.payloadType) {
throw new Error('You should select a payment method.');
}
switch (this.wire.payloadType) {
case 'onchain':
if (!this.wire.payload && !this.wire.payload.receiver) {
throw new Error('Invalid receiver.');
}
break;
case 'offchain':
if (this.balances.wireCap === null) {
// Skip client-side check until loaded
break;
}
const wireCap = this.balances.wireCap / Math.pow(10, 18),
balance = this.balances.offchain / Math.pow(10, 18);
if (this.wire.amount > wireCap) {
throw new VisibleWireError(
`You cannot spend more than ${wireCap} tokens today.`
);
} else if (this.wire.amount > balance) {
throw new VisibleWireError(
`You cannot spend more than ${balance} tokens.`
);
}
break;
}
if (!this.wire.guid) {
throw new Error('You cannot wire this.');
}
}
/**
* Checks if the user can submit using the current settings
*/
canSubmit() {
try {
this.validate();
return true;
} catch (e) {
// console.log(`Invalid wire: ${e.visible ? '[USERFACING] ' : ''}${e.message}`);
}
return false;
}
/**
* Shows visible wire errors
*/
showErrors() {
if (!this.submitted) {
this.error = '';
}
try {
this.validate();
} catch (e) {
if (e.visible) {
this.error = e.message;
}
}
}
buyTokens() {
this.overlayModal.dismiss();
this.router.navigate(['/token']);
}
/**
* Submits the wire payment
*/
async submit() {
if (this.inProgress) {
return;
}
if (!this.canSubmit()) {
this.showErrors();
return;
}
try {
this.inProgress = true;
this.submitted = true;
this.error = '';
if (
(await this.web3Wallet.isLocal()) &&
this.wire.payloadType === 'onchain'
) {
const action = await this.web3Wallet.setupMetamask();
switch (action) {
case GetMetamaskComponent.ACTION_CREATE:
this.router.navigate(['/wallet']);
this.inProgress = false;
this.overlayModal.dismiss();
break;
case GetMetamaskComponent.ACTION_CANCEL:
return;
}
}
let { done } = await this.wireService.submitWire(this.wire);
if (done) {
this.success = true;
if (this._opts && this._opts.onComplete) {
this._opts.onComplete(this.wire);
}
setTimeout(() => {
this.overlayModal.dismiss();
}, 2500);
}
} catch (e) {
this.error = (e && e.message) || 'Sorry, something went wrong';
} finally {
this.inProgress = false;
}
}
}
import { Injectable } from '@angular/core';
import { MindsUser } from '../../interfaces/entities';
import { EntitiesService } from '../../common/services/entities.service';
export type WirePaymentHandler = 'plus' | 'pro';
@Injectable()
export class WirePaymentHandlersService {
minds = window.Minds;
constructor(protected entities: EntitiesService) {}
async get(service: WirePaymentHandler): Promise<MindsUser> {
if (!this.minds.handlers || !this.minds.handlers[service]) {
throw new Error('Invalid handler definitions');
}
const response: any = await this.entities
.setCastToActivities(false)
.fetch([`urn:user:${this.minds.handlers[service]}`]);
if (!response || !response.entities || !response.entities[0]) {
throw new Error('Missing payment handler');
}
const handler = response.entities[0] as MindsUser;
if (!handler.guid || !handler.is_admin) {
throw new Error('Invalid payment target');
}
return handler;
}
}
......@@ -9,7 +9,6 @@ import { FaqModule } from '../faq/faq.module';
import { PaymentsModule } from '../payments/payments.module';
import { WireCreatorComponent } from './creator/creator.component';
import { WirePaymentsCreatorComponent } from './payments-creator/creator.component';
import { WireButtonComponent } from './button/button.component';
import { WireChannelComponent } from './channel/channel.component';
import { WireChannelTableComponent } from './channel/table/table.component';
......@@ -27,6 +26,8 @@ import { WireConsoleOverviewComponent } from './console/overview/overview.compon
import { WireConsoleRewardsInputsComponent } from './console/rewards-table/inputs/wire-console-rewards-inputs.component';
import { WireConsoleRewardsComponent } from './console/rewards-table/rewards.component';
import { WireSubscriptionTiersComponent } from './channel/tiers.component';
import { WirePaymentsCreatorComponent } from './creator/payments/payments.creator.component';
import { WirePaymentHandlersService } from './wire-payment-handlers.service';
const wireRoutes: Routes = [
{ path: 'wire', component: WireMarketingComponent },
......@@ -63,7 +64,7 @@ const wireRoutes: Routes = [
WireConsoleOverviewComponent,
WireSubscriptionTiersComponent,
],
providers: [WireService],
providers: [WireService, WirePaymentHandlersService],
exports: [
WireLockScreenComponent,
WireButtonComponent,
......
......@@ -143,6 +143,11 @@
"last_tos_update" => Minds\Core\Config::_()->get('last_tos_update') ?: time(),
"tags" => Minds\Core\Config::_()->get('tags') ?: [],
"pro" => $pro,
'handlers' => [
'plus' => Minds\Core\Di\Di::_()->get('Config')->get('plus')['handler'] ?? null,
'pro' => Minds\Core\Di\Di::_()->get('Config')->get('pro')['handler'] ?? null,
],
'upgrades' => Minds\Core\Di\Di::_()->get('Config')->get('upgrades'),
"environment" => getenv('MINDS_ENV') ?: 'development',
];
......
......@@ -28,6 +28,16 @@ interface Minds {
tags: string[];
pro?: any;
handlers?: { pro: string; plus: string };
upgrades?: {
pro: {
monthly: { tokens: number; usd: number };
yearly: { tokens: number; usd: number };
};
plus: {
monthly: { tokens: number; usd: number };
yearly: { tokens: number; usd: number };
};
};
}
interface MindsNavigation {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment