Commit ebeeb266 authored by Mark Harding's avatar Mark Harding

(feat): introduces upgrades page

1 merge request!590WIP: Epic/pro affiliate launch
Pipeline #88941471 pending with stages
......@@ -70,6 +70,7 @@ import { HttpClientModule } from '@angular/common/http';
import { AnalyticsModule } from './modules/analytics/analytics.module';
import { ProModule } from './modules/pro/pro.module';
import { ChannelContainerModule } from './modules/channel-container/channel-container.module';
import { UpgradesModule } from './modules/upgrades/upgrades.module';
import * as Sentry from '@sentry/browser';
......@@ -147,6 +148,7 @@ export class SentryErrorHandler implements ErrorHandler {
IssuesModule,
CanaryModule,
ChannelsModule,
UpgradesModule,
//last due to :username route
ChannelContainerModule,
......
......@@ -109,6 +109,10 @@ import { RouterHistoryService } from './services/router-history.service';
import { DraggableListComponent } from './components/draggable-list/list.component';
import { DndModule } from 'ngx-drag-drop';
import { SiteService } from './services/site.service';
import { MarketingComponent } from './components/marketing/marketing.component';
import { MarketingFooterComponent } from './components/marketing/footer.component';
import { ToggleComponent } from './components/toggle/toggle.component';
import { MarketingAsFeaturedInComponent } from './components/marketing/as-featured-in.component';
@NgModule({
imports: [
......@@ -207,6 +211,10 @@ import { SiteService } from './services/site.service';
FeaturedContentComponent,
PosterDateSelectorComponent,
DraggableListComponent,
ToggleComponent,
MarketingComponent,
MarketingFooterComponent,
MarketingAsFeaturedInComponent,
],
exports: [
MINDS_PIPES,
......@@ -294,6 +302,9 @@ import { SiteService } from './services/site.service';
PosterDateSelectorComponent,
ChannelModeSelectorComponent,
DraggableListComponent,
ToggleComponent,
MarketingComponent,
MarketingAsFeaturedInComponent,
],
providers: [
SiteService,
......
<div class="m-grid m-marketing__asFeaturedIn">
<div class="m-grid__column-2 m-marketingAsFeaturedIn__title" i18n>
As featured in
</div>
<ul class="m-grid__column-10">
<li class="m-marketingAsFeaturedIn__item--bigger">
<img [src]="cdnAssetsUrl + 'assets/marketing/bbc.png'" alt="BBC" />
</li>
<li>
<img
[src]="cdnAssetsUrl + 'assets/marketing/foxnews.png'"
alt="Fox News"
/>
</li>
<li>
<img [src]="cdnAssetsUrl + 'assets/marketing/forbes.png'" alt="Forbes" />
</li>
<li>
<img
[src]="cdnAssetsUrl + 'assets/marketing/techcrunch.png'"
alt="TechCrunch"
/>
</li>
<li>
<img
[src]="cdnAssetsUrl + 'assets/marketing/reuters.png'"
alt="Reuters"
/>
</li>
<li>
<img [src]="cdnAssetsUrl + 'assets/marketing/wired.png'" alt="Wired" />
</li>
<li>
<img
[src]="cdnAssetsUrl + 'assets/marketing/tjre.png'"
alt="The Joe Rogan Experience"
/>
</li>
</ul>
</div>
@import '../../../foundation/grid-values';
.m-marketing__asFeaturedIn {
max-width: 1084px;
margin: 40px auto 0;
@media screen and (max-width: $m-grid-min-vp) {
&.m-grid {
display: block;
}
margin: 20px 0 45px;
padding: 0 12px;
}
.m-marketingAsFeaturedIn__title {
display: flex;
flex-direction: column;
justify-content: center;
@include m-theme() {
color: themed($m-grey-400);
}
@media screen and (max-width: $m-grid-min-vp) {
text-align: center;
margin: 0 0 20px;
}
}
ul {
flex-grow: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 0;
padding: 0;
list-style: none;
> li {
img {
width: 55px;
height: 55px;
object-fit: contain;
@media screen and (max-width: $m-grid-min-vp) {
width: 40px;
height: 40px;
}
}
&.m-marketingAsFeaturedIn__item--bigger {
img {
width: 96px;
height: 96px;
@media screen and (max-width: $m-grid-min-vp) {
width: 40px;
height: 40px;
}
}
}
}
}
}
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'm-marketing__asFeaturedIn',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'as-featured-in.component.html',
})
export class MarketingAsFeaturedInComponent {
readonly cdnAssetsUrl: string = window.Minds.cdn_assets_url;
}
<div class="m-marketing__footer">
<div class="m-grid m-marketingFooter__columns">
<div
class="m-grid__column-3 m-grid__column-12--mobile m-marketingFooter__column"
>
<h4 i18n>About</h4>
<ul>
<li>
<a href="#" i18n>
Company
</a>
</li>
<li>
<a href="#" i18n>
Mission
</a>
</li>
<li>
<a href="#" i18n>
Features
</a>
</li>
<li>
<a routerLink="/mobile" i18n>
Mobile
</a>
</li>
<li>
<a routerLink="/jobs" i18n>
Careers
</a>
</li>
<li>
<a href="#" i18n>
Investors
</a>
</li>
<li>
<a routerLink="/blog/minds" i18n>
Blog
</a>
</li>
<li>
<a
href="https://cdn-assets.minds.com/front/dist/assets/whitepapers/03_27_18_Minds%20Whitepaper%20V0.1.pdf"
target="_blank"
i18n
>
Whitepaper
</a>
</li>
</ul>
</div>
<div
class="m-grid__column-3 m-grid__column-12--mobile m-marketingFooter__column"
>
<h4 i18n>Business</h4>
<ul>
<li>
<a routerLink="/upgrades" i18n>
Upgrade
</a>
</li>
<li>
<a routerLink="/token" i18n>
Token
</a>
</li>
<li>
<a routerLink="/plus" i18n>
Plus
</a>
</li>
<li>
<a routerLink="/pro" i18n>
Pro
</a>
</li>
<li>
<a routerLink="/nodes" i18n>
Nodes
</a>
</li>
<li>
<a routerLink="/boost" i18n>
Boost
</a>
</li>
<li>
<a href="#" i18n>
Payments
</a>
</li>
<li>
<a href="#" i18n>
Store
</a>
</li>
</ul>
</div>
<div
class="m-grid__column-3 m-grid__column-12--mobile m-marketingFooter__column"
>
<h4>Developers</h4>
<ul>
<li>
<a href="https://developers.minds.com" target="_blank" i18n>
Documentation
</a>
</li>
<li>
<a routerLink="/groups/profile/365903183068794880" i18n>
Community
</a>
</li>
<li>
<a href="https://gitlab.com/minds" target="_blank" i18n>
Code
</a>
</li>
<li>
<a routerLink="/canary" i18n>
Canary
</a>
</li>
<li>
<a routerLink="/branding" i18n>
Branding
</a>
</li>
</ul>
</div>
<div
class="m-grid__column-3 m-grid__column-12--mobile m-marketingFooter__column"
>
<h4>Support</h4>
<ul>
<li>
<a routerLink="/help" i18n>
Help Desk
</a>
</li>
<li>
<a routerLink="/groups/profile/100000000000000681" i18n>
Community
</a>
</li>
<li>
<a routerLink="/localization" i18n>
Languages
</a>
</li>
<li>
<a href="https://status.minds.com" target="_blank" i18n>
Status
</a>
</li>
<li>
<a routerLink="/p/contact" i18n>
Contact
</a>
</li>
</ul>
</div>
</div>
<div class="m-marketing__sep m-marketing__sep--big"></div>
<div class="m-grid m-marketingFooter__columns">
<div
class="m-grid__column-2 m-grid__column-12--mobile m-marketingFooter__column m-marketingFooter__column--noMobileSpacing"
i18n
>
<div class="m-marketingFooter__text">&copy; {{ year }} Minds, Inc.</div>
</div>
<div
class="m-grid__column-10 m-grid__column-12--mobile m-marketingFooter__column"
>
<ul class="m-marketingFooter__inlineList">
<li>
<a routerLink="/p/terms" i18n>
Terms of Service
</a>
</li>
<li>
<a routerLink="/p/privacy" i18n>
Privacy Policy
</a>
</li>
<li>
<a routerLink="/content-policy" i18n>
Content Policy
</a>
</li>
<li>
<a routerLink="/p/dmca" i18n>
DMCA
</a>
</li>
</ul>
</div>
</div>
</div>
@import '../../../foundation/grid-values';
m-marketing__footer {
display: block;
margin-top: 105px;
@media screen and (max-width: $m-grid-min-vp) {
margin-top: 80px;
}
@include m-theme() {
background: linear-gradient(
180deg,
themed($m-marketing-bg-gradient-start) 0%,
themed($m-white) 100%
);
}
}
.m-marketing__footer {
padding: 60px 0 48px;
@media screen and (max-width: $m-grid-min-vp) {
padding: 32px 0;
}
@include m-theme() {
color: themed($m-grey-800);
}
.m-marketingFooter__columns {
max-width: 1084px;
margin: 0 auto;
@media screen and (max-width: $m-grid-min-vp) {
padding: 0 32px;
}
}
.m-marketingFooter__column {
@media screen and (max-width: $m-grid-min-vp) {
margin-bottom: 32px;
&:last-child,
&.m-marketingFooter__column--noMobileSpacing {
margin-bottom: 0;
}
}
h4 {
font-weight: 500;
font-size: 16px;
line-height: 21px;
margin: 0 0 26px;
@media screen and (max-width: $m-grid-min-vp) {
margin: 0 0 8px;
}
@include m-theme() {
color: themed($m-grey-800);
}
}
.m-marketingFooter__text {
font-size: 14px;
line-height: 26px;
}
ul {
list-style: none;
margin: 0;
padding: 0;
> li {
@extend .m-marketingFooter__text;
@include m-theme() {
color: themed($m-grey-300);
}
@media screen and (max-width: $m-grid-min-vp) {
display: inline-block;
margin-right: 1em;
&:last-child {
margin-right: 0;
}
}
a {
color: inherit;
font-weight: 300;
text-decoration: none;
}
}
&.m-marketingFooter__inlineList {
> li {
display: inline-block;
margin-right: 40px;
@media screen and (max-width: $m-grid-min-vp) {
margin-right: 1em;
}
&:last-child {
margin-right: 0;
}
}
}
}
}
}
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'm-marketing__footer',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'footer.component.html',
})
export class MarketingFooterComponent {
readonly year: number = new Date().getFullYear();
}
.m-marketing__main,
.m-marketing__section {
// Common
.m-marketing--hideMobile {
@media screen and (max-width: $m-grid-min-vp) {
display: none;
}
}
.m-marketing__title,
h1 {
font-size: 22px;
line-height: 44px;
font-weight: bold;
opacity: 0.7;
margin: 12px 0 4px;
@include m-theme() {
color: themed($m-grey-800);
}
}
.m-marketing__subtitle,
h2 {
font-weight: 900;
font-size: 42px;
line-height: 44px;
margin: 0 0 23px;
position: relative;
z-index: 0;
@media screen and (max-width: $m-grid-min-vp) {
font-size: 32px;
line-height: 34px;
margin: 0 0 18px;
}
&.m-marketing__subtitle--asTitle {
font-size: 48px;
line-height: 53px;
margin: 0 0 26px;
@media screen and (max-width: $m-grid-min-vp) {
font-size: 38px;
line-height: 46px;
margin: 0 0 21px;
}
}
em {
font-style: inherit;
text-decoration: inherit;
white-space: nowrap;
position: relative;
&::after {
content: '';
display: inline-block;
position: absolute;
background: rgba(80, 226, 195, 0.3);
top: 0.36em;
left: -0.03em;
right: -0.03em;
bottom: 0.22em;
pointer-events: none;
z-index: -1;
}
}
em + em::after {
left: -0.25em;
}
}
p.m-marketing__description {
font-size: 18px;
line-height: 27px;
margin: 0 0 36px;
}
ul.m-marketing__points {
list-style: disc;
margin: 0 0 45px;
padding: 0 0 0 1em;
font-size: 16px;
line-height: 21px;
> li {
margin-bottom: 19px;
&:last-child {
margin-bottom: 0;
}
}
}
// Style 1
&.m-marketing__section--style-1 {
@include m-theme() {
background: linear-gradient(
180deg,
themed($m-white) 0%,
themed($m-marketing-bg-gradient-start) 100%
);
}
.m-marketing__wrapper {
padding: 95px 0 85px;
@media screen and (max-width: $m-grid-min-vp) {
padding: 30px 0;
margin: 0 20px;
}
}
p.m-marketing__description {
@include m-theme() {
color: themed($m-grey-300);
}
}
}
// Style 2
&.m-marketing__section--style-2 {
.m-marketing__wrapper {
position: relative;
z-index: 0;
padding: 72px 0 32px;
@media screen and (max-width: $m-grid-min-vp) {
padding: 0;
}
}
.m-marketing__body {
position: relative;
@media screen and (max-width: $m-grid-min-vp) {
padding: 0 30px 30px;
}
&::before {
content: '';
position: absolute;
top: 0;
right: -290px;
bottom: -56px;
left: 0;
transform: translate(-86px, -56px);
clip-path: polygon(0% 0%, 0% 100%, 100% 92%, 100% 0%);
z-index: -1;
@include m-theme() {
background: linear-gradient(
180deg,
themed($m-marketing-bg-colored-gradient-start) 0%,
themed($m-marketing-bg-colored-gradient-end) 99.99%
);
}
@media screen and (max-width: $m-grid-min-vp) {
right: 0;
bottom: -3vw;
transform: none;
clip-path: none;
}
}
h1 {
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
margin: 15px 0 15px;
text-align: center;
}
}
h2 {
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
font-size: 28px;
line-height: 32px;
margin: 0 0 17px;
text-align: center;
}
}
}
p.m-marketing__description {
margin-bottom: 42px;
padding-right: 200px;
@include m-theme() {
color: themed($m-grey-300);
}
@include m-on-theme(dark) {
color: #ffffff;
}
@media screen and (max-width: $m-grid-min-vp) {
padding-right: 0;
margin-bottom: 30px;
font-size: 16px;
line-height: 23px;
text-align: center;
}
}
.m-marketing__image {
position: relative;
z-index: 0;
img {
object-fit: contain;
clip-path: polygon(0% 1%, 0% 97%, 100% 100%, 100% 0%);
@media screen and (max-width: $m-grid-min-vp) {
width: 100vw;
height: 100vw;
object-fit: cover;
clip-path: polygon(0% 2%, 0% 97%, 100% 100%, 100% 0%);
}
}
span {
display: inline-block;
position: relative;
// Deco
&::before {
content: '';
display: block;
position: absolute;
width: 393px;
height: 193px;
bottom: 0;
right: 0;
transform: translate(45px, 32px);
background: url('<%= APP_CDN %>/assets/marketing/deco_1.svg')
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
content: initial;
display: none;
}
}
&::after {
content: '';
display: block;
position: absolute;
top: 0;
right: 0;
width: 284px;
height: 262px;
transform: translate(35px, -35px);
background: url('<%= APP_CDN %>/assets/marketing/deco_2-straight.svg')
no-repeat;
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
content: initial;
display: none;
}
}
}
}
}
}
<div class="m-marketing">
<div class="m-marketing__mainWrapper">
<ng-content select=".m-marketing__main"></ng-content>
</div>
<ng-content select="[slot=2]"></ng-content>
<div class="m-marketing__sep" *ngIf="bodyWrapper?.children?.length > 0"></div>
<div class="m-marketing__extrasWrapper" #bodyWrapper>
<ng-content select=".m-marketing__extras"></ng-content>
</div>
</div>
<m-marketing__footer></m-marketing__footer>
@import '../../../foundation/grid-values';
m-marketing {
display: block;
font-family: Roboto, sans-serif;
@include m-theme() {
background: themed($m-white);
color: themed($m-grey-800);
}
.m-marketing {
font-family: Roboto, sans-serif;
font-weight: 400;
}
.m-marketing__sep {
border-top: 1px solid;
height: 0;
width: 100%;
margin: 40px 0;
&.m-marketing__sep--big {
margin: 60px 0;
}
@media screen and (max-width: $m-grid-min-vp) {
margin: 20px 0;
&.m-marketing__sep--big {
margin: 30px 0;
}
}
@include m-theme() {
border-color: themed($m-grey-50);
}
}
.m-marketing__wrapper,
.m-marketing__extras > * > * {
max-width: 1084px;
margin: 0 auto;
box-sizing: border-box;
}
}
import {
ChangeDetectionStrategy,
Component,
Input,
OnInit,
} from '@angular/core';
import { MindsTitle } from '../../../services/ux/title';
@Component({
selector: 'm-marketing',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'marketing.component.html',
})
export class MarketingComponent implements OnInit {
@Input() pageTitle: string = '';
constructor(protected title: MindsTitle) {}
ngOnInit() {
if (this.pageTitle) {
this.title.setTitle(this.pageTitle);
}
}
}
<div class="m-toggle__track"></div>
<div
class="m-toggle__switch"
[class.m-toggle__switch--left]="mModel === leftValue"
[class.m-toggle__switch--right]="mModel === rightValue"
></div>
m-toggle {
position: relative;
display: inline-block;
width: 27px;
height: 19px;
margin: 0 0.35em;
cursor: pointer;
.m-toggle__track {
display: inline-block;
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 10px;
z-index: 1;
transform: translateY(-50%);
border-radius: 6px;
@include m-theme() {
background: themed($m-grey-100);
}
}
.m-toggle__switch {
display: inline-block;
position: absolute;
top: 0;
left: 50%;
width: 19px;
height: 19px;
z-index: 2;
transform: translateX(-50%);
border-radius: 50%;
@include m-theme() {
background: themed($m-grey-100);
box-shadow: 1px 1px 1px -1px rgba(themed($m-black), 0.3);
}
&.m-toggle__switch--left,
&.m-toggle__switch--right {
transform: none;
@include m-theme() {
background: themed($m-blue);
}
}
&.m-toggle__switch--left {
left: 0;
right: auto;
}
&.m-toggle__switch--right {
right: 0;
left: auto;
}
}
}
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
HostListener,
Input,
Output,
} from '@angular/core';
@Component({
selector: 'm-toggle',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'toggle.component.html',
})
export class ToggleComponent {
@Input('leftValue') leftValue: any;
@Input('rightValue') rightValue: any;
@Input('mModel') mModel: any;
@Output('mModelChange') mModelChange: EventEmitter<any> = new EventEmitter<
any
>();
@HostListener('click') toggle() {
if (this.mModel === this.leftValue) {
this.mModelChange.emit(this.rightValue);
} else {
this.mModelChange.emit(this.leftValue);
}
}
}
@import './grid-values';
@import '../../stylesheets/themes';
.mf-button {
display: inline-block;
padding: 14px 32px;
font: inherit;
font-size: 18px;
line-height: 24px;
border: 1px solid;
border-radius: 2px;
font-weight: 400;
cursor: pointer;
user-select: none;
white-space: nowrap;
text-align: center;
appearance: none;
text-decoration: none;
@include m-theme() {
background: themed($m-blue);
color: themed($m-white-always);
border-color: themed($m-blue);
}
&.mf-button--alt {
@include m-theme() {
background: themed($m-aqua);
border-color: themed($m-aqua);
color: themed($m-white-always);
}
}
@media screen and (max-width: $m-grid-min-vp) {
display: block;
font-size: 15px;
padding: 12px 15px;
width: 100%;
box-sizing: border-box;
}
&.mf-button--always-inline {
@media screen and (max-width: $m-grid-min-vp) {
display: inline-block;
}
}
&.mf-button--hollow {
@include m-theme() {
background: themed($m-white);
color: themed($m-black);
border-color: themed($m-blue);
}
}
}
$m-grid-min-vp: 1168px;
$m-grid-cols: 12;
$m-grid-gap: 20px;
@import './grid-values';
.m-grid {
display: grid;
grid-template-columns: repeat($m-grid-cols, 1fr);
grid-column-gap: $m-grid-gap;
grid-row-gap: 0;
@for $i from 1 through $m-grid-cols {
.m-grid__column-#{$i} {
grid-column: auto / span $i;
}
}
@media screen and (max-width: $m-grid-min-vp) {
@for $i from 1 through $m-grid-cols {
.m-grid__column-#{$i}--mobile {
grid-column: auto / span $i;
}
}
}
}
.mf-jumpAnchor {
// used for element.scrollIntoView targets
position: relative;
top: 0;
width: 0;
height: 0;
visibility: hidden;
m-body.has-v2-navbar & {
top: -52px;
}
}
export type Currency = 'tokens' | 'usd';
export default function currency(value: number, type: Currency) {
switch (type) {
case '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>;
......
<button
class="mdl-button mdl-button--colored mdl-color--green"
*ngIf="user && !isPlus()"
(click)="purchase(20, 'month')"
(click)="purchase()"
[hidden]="active"
>
Upgrade - 20 Tokens/month
</button>
<button
class="mdl-button mdl-button--colored mdl-color--green"
*ngIf="user && !isPlus() && false"
(click)="purchase(200, 'year')"
[hidden]="active"
>
Upgrade - 200 Tokens/year
Upgrade - {{ minds.upgrades.plus['monthly']['tokens'] }} Tokens/month
</button>
<!--<button-->
<!-- class="mdl-button mdl-button&#45;&#45;colored mdl-color&#45;&#45;green"-->
<!-- *ngIf="user && !isPlus() && false"-->
<!-- (click)="purchase(200, 'year')"-->
<!-- [hidden]="active"-->
<!--&gt;-->
<!-- Upgrade - 200 Tokens/year-->
<!--</button>-->
<button
class="mdl-button mdl-button--colored mdl-color--green"
*ngIf="user && isPlus()"
......@@ -27,7 +27,7 @@
*ngIf="!user"
routerLink="/login"
>
Upgrade - 200 Tokens/year
Upgrade - {{ minds.upgrades.plus['monthly']['tokens'] }} Tokens/month
</button>
<div class="m-plus--subscription mdl-card mdl-shadow--4dp" *ngIf="active">
......
///<reference path="../../../../node_modules/@types/jasmine/index.d.ts"/>
import {
async,
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import {
Component,
DebugElement,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Client } from '../../services/api/client';
import { clientMock } from '../../../tests/client-mock.spec';
import { PlusVerify } from '../../mocks/modules/plus/verify';
import { FooterComponentMock } from '../../mocks/modules/plus/footer';
import { FaqMock } from '../../mocks/modules/plus/faq';
import { PlusSubscription } from '../../mocks/modules/plus/subscription';
import { PlusSubscriptionComponent } from './subscription.component';
import { RouterTestingModule } from '@angular/router/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { SignupModalService } from '../../modules/modals/signup/service';
import { OverlayModalService } from '../../services/ux/overlay-modal';
import { overlayModalServiceMock } from '../../../tests/overlay-modal-service-mock.spec';
import { WireStruc } from '../wire/creator/creator.component';
import { Session } from '../../services/session';
import { sessionMock } from '../../../tests/session-mock.spec';
import { WireService } from '../../modules/wire/wire.service';
import { Web3WalletService } from '../blockchain/web3-wallet.service';
import { TokenContractService } from '../blockchain/contracts/token-contract.service';
import { tokenContractServiceMock } from '../../../tests/token-contract-service-mock.spec';
import { MaterialMock } from '../../../tests/material-mock.spec';
import { MaterialSwitchMock } from '../../../tests/material-switch-mock.spec';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { By } from '@angular/platform-browser';
let web3WalletServiceMock = new (function() {
this.wallets = ['0x123', '0x1234'];
this.balance = 127000000000000000000;
this.onChainInterfaceLabel = 'Metamask';
this.unavailable = false;
this.locked = false;
this.isUnavailable = jasmine.createSpy('isUnavailable').and.callFake(() => {
return this.unavailable;
});
this.unlock = jasmine.createSpy('unlock').and.callFake(async () => {
return !this.locked;
});
this.ready = jasmine.createSpy('ready').and.callFake(async () => {
return true;
});
this.getWallets = jasmine.createSpy('getWallets').and.callFake(async () => {
return this.wallets;
});
this.getCurrentWallet = jasmine
.createSpy('getCurrentWallet')
.and.callFake(async () => {
return this.wallets[0];
});
this.getBalance = jasmine.createSpy('getBalance').and.callFake(async () => {
return this.balance;
});
this.getOnChainInterfaceLabel = jasmine
.createSpy('getOnChainInterfaceLabel')
.and.callFake(() => {
return this.onChainInterfaceLabel
? this.onChainInterfaceLabel
: 'Metamask';
});
})();
let wireServiceMock = new (function() {
this.wireSent = new EventEmitter<any>();
this.submitWire = jasmine
.createSpy('submitWire')
.and.callFake(async (wireStruc: WireStruc) => {
return { success: true };
});
})();
let signupServiceMock = new (function() {
this.initOnScroll = jasmine.createSpy('initOnScroll').and.stub();
this.open = jasmine.createSpy('open').and.stub();
this.close = jasmine.createSpy('close').and.stub();
})();
@Component({
selector: 'm--crypto-token-symbol',
template: '',
})
export class MindsTokenSymbolComponent {}
@Component({
selector: 'minds-payments-stripe-checkout',
outputs: ['inputed', 'done'],
template: '',
})
export class StripeCheckoutMock {
inputed: EventEmitter<any> = new EventEmitter();
done: EventEmitter<any> = new EventEmitter();
@Input() amount: number = 0;
@Input() merchant_guid;
@Input() gateway: string = 'merchants';
@Input('useMDLStyling') useMDLStyling: boolean = true;
@Input() useCreditCard: boolean = true;
@Input() useBitcoin: boolean = false;
}
describe('PlusSubscriptionComponent', () => {
let comp: PlusSubscriptionComponent;
let fixture: ComponentFixture<PlusSubscriptionComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
MaterialMock,
MaterialSwitchMock,
PlusSubscriptionComponent,
PlusSubscription,
MindsTokenSymbolComponent,
StripeCheckoutMock,
PlusVerify,
FaqMock,
],
imports: [RouterTestingModule, FormsModule, ReactiveFormsModule],
providers: [
{ provide: Web3WalletService, useValue: web3WalletServiceMock },
{ provide: Client, useValue: clientMock },
{ provide: WireService, useValue: wireServiceMock },
{ provide: Session, useValue: sessionMock },
{ provide: SignupModalService, useValue: signupServiceMock },
{ provide: TokenContractService, useValue: tokenContractServiceMock },
{ provide: OverlayModalService, useValue: overlayModalServiceMock },
],
}).compileComponents();
}));
beforeEach(done => {
jasmine.MAX_PRETTY_PRINT_DEPTH = 10;
jasmine.clock().uninstall();
jasmine.clock().install();
fixture = TestBed.createComponent(PlusSubscriptionComponent);
window.Minds.blockchain = {
plus_address: 'oxtn',
};
comp = fixture.componentInstance;
window.Minds.user = {
guid: '732337264197111809',
type: 'user',
subtype: false,
time_created: '1499978809',
time_updated: false,
container_guid: '0',
owner_guid: '0',
site_guid: false,
access_id: '2',
name: 'minds',
username: 'minds',
language: 'en',
icontime: '1506690756',
legacy_guid: false,
featured_id: false,
banned: 'no',
website: '',
dob: '',
gender: '',
city: '',
merchant: {},
boostProPlus: false,
fb: false,
mature: 0,
monetized: '',
signup_method: false,
social_profiles: [],
feature_flags: false,
programs: ['affiliate'],
plus: true,
verified: false,
disabled_boost: false,
show_boosts: false,
chat: true,
subscribed: false,
subscriber: false,
subscriptions_count: 1,
impressions: 10248,
boost_rating: '2',
spam: 0,
deleted: 0,
};
// Set up mock HTTP client
clientMock.response = {};
clientMock.response['api/v1/plus'] = {
status: 'success',
active: false,
};
clientMock.response['api/v1/plus/subscription'] = {
status: 'success',
active: true,
};
fixture.detectChanges();
if (fixture.isStable()) {
done();
} else {
fixture.whenStable().then(() => {
done();
});
}
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('Should load correctly', fakeAsync(() => {
comp.user = {
feature_flags: false,
programs: ['affiliate'],
plus: true,
verified: false,
disabled_boost: false,
show_boosts: false,
chat: true,
subscribed: false,
};
const subscription = fixture.debugElement.query(
By.css('.m-plus--subscription')
);
expect(subscription).toBeNull();
fixture.detectChanges();
expect(comp.isPlus()).toBe(true);
comp.cancel();
tick();
fixture.detectChanges();
expect(comp.isPlus()).toBe(false);
}));
it('Should load using the proper endpoint', () => {
comp.load();
fixture.detectChanges();
expect(comp.isPlus()).toBe(true);
expect(clientMock.get.calls.mostRecent().args[0]).toEqual('api/v1/plus');
});
it('Should load correctly plus is false', fakeAsync(() => {
comp.user = {
feature_flags: false,
programs: ['affiliate'],
plus: false,
verified: false,
disabled_boost: false,
show_boosts: false,
chat: true,
subscribed: false,
};
const subscription = fixture.debugElement.query(
By.css('.m-plus--subscription')
);
expect(subscription).toBeNull();
fixture.detectChanges();
expect(comp.isPlus()).toBe(false);
comp.purchase(20, 'month');
tick();
fixture.detectChanges();
expect(overlayModalServiceMock.create).toHaveBeenCalled();
}));
});
......@@ -6,6 +6,8 @@ import {
Output,
Input,
} from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Subscription } from 'rxjs';
import { Client } from '../../common/api/client.service';
import { Web3WalletService } from '../blockchain/web3-wallet.service';
......@@ -14,8 +16,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',
......@@ -32,8 +35,12 @@ export class PlusSubscriptionComponent {
active: boolean = false;
@Output('completed') completed$: EventEmitter<any> = new EventEmitter();
@Input('showSubscription') showSubscription: boolean;
payment: any = {};
payload: any;
minds = window.Minds;
currency: string;
interval: string;
paramSubscription: Subscription;
constructor(
private client: Client,
......@@ -42,10 +49,23 @@ export class PlusSubscriptionComponent {
private web3Wallet: Web3WalletService,
private overlayModal: OverlayModalService,
private modal: SignupModalService,
private wirePaymentHandlers: WirePaymentHandlersService,
public session: Session,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
private route: ActivatedRoute
) {}
ngOnInit() {
this.paramSubscription = this.route.queryParams.subscribe(
(params: Params) => {
this.currency = params.c || 'tokens';
this.interval = params.i || 'yearly';
if (params.c || params.i) this.purchase();
}
);
}
load(): Promise<any> {
return this.client
.get('api/v1/plus')
......@@ -68,23 +88,19 @@ export class PlusSubscriptionComponent {
this.purchase();
}
async purchase(amount: number = 20, period: 'month' | 'year' = 'month') {
async purchase(/*amount: number = 20, period: 'month' | 'year' = 'month'*/) {
if (!this.session.isLoggedIn()) {
this.modal.open();
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,
interval: this.interval,
currency: this.currency,
amount: this.minds.upgrades.plus[this.interval][this.currency],
onComplete: wire => {
this.completed = true;
this.user.plus = true;
......@@ -115,4 +131,8 @@ export class PlusSubscriptionComponent {
this.cd.markForCheck();
this.cd.detectChanges();
}
ngOnDestroy() {
this.paramSubscription.unsubscribe();
}
}
<div *ngIf="isLoggedIn">
<ng-container *ngIf="!inProgress; else inProgressSpinner">
<button
[hidden]="true"
*ngIf="!active"
class="mdl-button mdl-button--colored mdl-color--green"
[disabled]="inProgress || criticalError"
(click)="enable()"
>
Become Pro
</button>
<button
*ngIf="active"
class="mdl-button mdl-button--colored mdl-color--red"
[disabled]="inProgress || criticalError"
(click)="disable()"
>
Cancel Pro
</button>
<span *ngIf="error" class="m-proSubscription__error">
{{ error }}
</span>
</ng-container>
<ng-template #inProgressSpinner>
<div class="m-proSubscription__inProgress">
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</div>
</ng-template>
</div>
m-pro--subscription {
.m-proSubscription__error {
display: block;
font-weight: bold;
color: #fff;
margin: 8px 0 0 5px;
}
}
<div class="m-marketing m-marketing__Pro">
<div class="m-marketing--hero">
<div class="m-marketing--hero--video">
<img [src]="minds.cdn_assets_url + 'assets/photos/fractal.jpg'" />
</div>
<div class="m-marketing--hero--inner">
<div class="m-marketing--hero--overlay"></div>
<m-marketing class="m-pro__marketing" pageTitle="Minds Pro" i18n-pageTitle>
<div class="m-marketing__main m-marketing__section--style-2">
<div class="m-grid m-marketing__wrapper">
<div class="m-grid__column-7 m-grid__column-12--mobile m-marketing__body">
<h1 i18n>
Minds Pro
</h1>
<div class="m-marketing--hero--slogans">
<h1 i18n>Minds Pro</h1>
<h2 ngPreserveWhitespaces i18n>
The ultimate platform for <em>creators</em> <em>and</em>
<em>brands</em>
</h2>
<h3 i18n>
The ultimate platform for independent content creators on the
Internet.
</h3>
<p class="m-marketing__description" i18n>
Earn revenue for your content and upgrade your channel into an
independent website with all the professional tools you need to do
what you love.
</p>
<ul>
<li i18n>Unlimited HD video</li>
<li i18n>Custom domains</li>
<li i18n>Complete monetization</li>
<li i18n>Full analytics console</li>
<li i18n>Access to every Minds user</li>
<li i18n>Custom branding</li>
</ul>
<div class="">
<m-pro--subscription
(onEnable)="goToSettings()"
></m-pro--subscription>
</div>
</div>
<div class="m-marketing--hero--actions m-marketing--action-button">
<m-pro--subscription (onEnable)="goToSettings()"></m-pro--subscription>
<div
class="m-grid__column-5 m-grid__column-12--mobile m-marketing__image"
>
<span>
<img [src]="cdnAssetsUrl + 'assets/photos/podcast-people.jpg'" />
</span>
</div>
</div>
</div>
</div>
<m-footer></m-footer>
<m-marketing__asFeaturedIn slot="2"></m-marketing__asFeaturedIn>
</m-marketing>
......@@ -7,7 +7,7 @@ import { Router } from '@angular/router';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProMarketingComponent {
minds = window.Minds;
readonly cdnAssetsUrl: string = window.Minds.cdn_assets_url;
constructor(protected router: Router) {}
......
......@@ -5,7 +5,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '../../common/common.module';
import { ProService } from './pro.service';
import { ProMarketingComponent } from './marketing.component';
import { ProSubscriptionComponent } from './channel/subscription/subscription.component';
import { ProSubscriptionComponent } from './subscription/subscription.component';
import { ProChannelComponent } from './channel/channel.component';
import { ProChannelLoginComponent } from './channel/login/login.component';
import { MindsFormsModule } from '../forms/forms.module';
......
......@@ -17,12 +17,6 @@ export class ProService {
return Boolean(result.isActive);
}
async enable(): Promise<boolean> {
// TODO: Payments
await this.client.post('api/v2/pro');
return true;
}
async disable(): Promise<boolean> {
await this.client.delete('api/v2/pro');
return true;
......
<div *ngIf="isLoggedIn || true">
<ng-container *ngIf="!inProgress; else inProgressSpinner">
<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"
class="mdl-button mdl-button--colored mdl-color--red"
[disabled]="inProgress || criticalError"
(click)="disable()"
>
Cancel Pro
</button>
<span *ngIf="error" class="m-proSubscription__error">
{{ error }}
</span>
</ng-container>
<ng-template #inProgressSpinner>
<div class="m-proSubscription__inProgress">
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</div>
</ng-template>
</div>
......@@ -6,8 +6,19 @@ import {
OnInit,
Output,
} from '@angular/core';
import { Session } from '../../../../services/session';
import { ProService } from '../../pro.service';
import { ActivatedRoute, Params } from '@angular/router';
import { Subscription } from 'rxjs';
import { Session } from '../../../services/session';
import { ProService } from '../pro.service';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
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',
......@@ -19,6 +30,12 @@ export class ProSubscriptionComponent implements OnInit {
@Output() onDisable: EventEmitter<any> = new EventEmitter();
interval: UpgradeOptionInterval = 'yearly';
currency: UpgradeOptionCurrency = 'usd';
paramSubscription: Subscription;
isLoggedIn: boolean = false;
inProgress: boolean = false;
......@@ -34,7 +51,10 @@ export class ProSubscriptionComponent implements OnInit {
constructor(
protected service: ProService,
protected session: Session,
protected cd: ChangeDetectorRef
protected overlayModal: OverlayModalService,
protected wirePaymentHandlers: WirePaymentHandlersService,
protected cd: ChangeDetectorRef,
protected route: ActivatedRoute
) {}
ngOnInit() {
......@@ -43,6 +63,15 @@ export class ProSubscriptionComponent implements OnInit {
if (this.isLoggedIn) {
this.load();
}
this.paramSubscription = this.route.queryParams.subscribe(
(params: Params) => {
this.currency = params.c || 'usd';
this.interval = params.i || 'yearly';
if (params.c || params.i) this.enable();
}
);
}
async load() {
......@@ -67,17 +96,35 @@ export class ProSubscriptionComponent implements OnInit {
this.detectChanges();
try {
await this.service.enable();
this.active = true;
this.minds.user.pro = true;
this.onEnable.emit(Date.now());
this.overlayModal
.create(
WirePaymentsCreatorComponent,
await this.wirePaymentHandlers.get('pro'),
{
interval: this.interval,
currency: this.currency,
amount: this.minds.upgrades.pro[this.interval][this.currency],
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();
})
.present();
} catch (e) {
this.active = false;
this.minds.user.pro = false;
this.error = (e && e.message) || 'Unknown error';
this.inProgress = false;
}
this.inProgress = false;
this.detectChanges();
}
......@@ -101,8 +148,35 @@ 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();
}
ngOnDestroy() {
this.paramSubscription.unsubscribe();
}
}
@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;
margin: 8px 0 0 5px;
@include m-theme() {
color: themed($m-red);
}
}
}
<ng-template #buyTokensRateTpl>
32 tokens = 32,000 page views
</ng-template>
<div class="m-upgrades__buyTokens">
<div class="m-upgradesBuyTokens__wrapper">
<div class="m-upgradesBuyTokens__body">
<h2>Buy Tokens</h2>
<p class="m-upgradesBuyTokens__description">
Expand your reach with Boost and support your favorite channels with
Wire.
</p>
<p>
<a routerLink="/token">Learn more about tokens</a>
</p>
</div>
<div class="m-upgradesBuyTokens__form">
<div class="upgradesBuyTokens__amount">
<label for="m-upgradesBuyTokens__amountInput">$</label>
<input
type="number"
name="amount"
id="m-upgradesBuyTokens__amountInput"
/>
</div>
<div class="upgradesBuyTokens__rate">
<ng-container *ngTemplateOutlet="buyTokensRateTpl"></ng-container>
</div>
<div class="upgradesBuyTokens__action">
<button class="mf-button">Buy Tokens</button>
</div>
</div>
<div class="m-upgradesBuyTokens__overflownRate">
<ng-container *ngTemplateOutlet="buyTokensRateTpl"></ng-container>
</div>
</div>
</div>
m-upgrades__buyTokens {
display: block;
}
.m-upgrades__buyTokens {
margin-bottom: 105px;
.m-upgradesBuyTokens__wrapper {
padding: 40px 60px 60px;
background: url('<%= APP_CDN %>/assets/marketing/buytokens_bg.jpg')
no-repeat right center #726061;
background-size: contain;
color: #ffffff;
@media screen and (max-width: $m-grid-min-vp) {
background-image: none;
padding: 30px;
margin: 0 20px;
}
}
.m-upgradesBuyTokens__body {
max-width: 320px;
}
h2 {
margin: 0 0 12px;
font-size: 32px;
font-weight: 900;
line-height: 43px;
}
p {
a {
color: #4690df !important;
text-decoration: underline;
font-weight: 400;
}
&.m-upgradesBuyTokens__description {
margin-bottom: 25px;
opacity: 0.6;
}
}
.m-upgradesBuyTokens__form {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
max-width: 640px;
border-radius: 2px;
padding: 20px 35px;
margin-top: 40px;
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.07);
@include m-theme() {
background: themed($m-white);
color: themed($m-black);
}
.upgradesBuyTokens__rate {
@include m-theme() {
color: themed($m-grey-300);
}
@media screen and (max-width: $m-grid-min-vp) {
display: none;
}
}
.upgradesBuyTokens__amount {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
font-size: 24px;
label {
display: inline-block;
font: inherit;
margin-right: 0.1em;
@include m-theme() {
color: themed($m-grey-300);
}
}
input {
display: inline-block;
font: inherit;
appearance: none;
border: none;
margin: 0;
padding: 0;
line-height: 1;
vertical-align: top;
border-radius: 0;
width: 4em;
&:focus {
outline: none !important;
}
@include m-theme() {
color: themed($m-black);
&:focus {
box-shadow: 0 1px 0 rgba(themed($m-black), 0.2);
}
}
@media screen and (max-width: $m-grid-min-vp) {
max-width: calc(100% - 20px);
}
}
}
}
.m-upgradesBuyTokens__overflownRate {
display: none;
margin: 24px 0 0;
color: #9b9b9b;
@media screen and (max-width: $m-grid-min-vp) {
display: block;
}
}
}
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'm-upgrades__buyTokens',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'buy-tokens.component.html',
})
export class BuyTokensComponent {
readonly cdnAssetsUrl: string = window.Minds.cdn_assets_url;
}
<div class="m-upgrades__upgradeOptions">
<div class="m-upgradesUpgradeOptions__header">
<h2>Upgrade Options</h2>
<div class="m-upgradesUpgradeOptionsHeader__toggleContainer">
<div class="m-upgradesUpgradeOptionsHeader__toggle">
<span>Yearly</span>
<span
><m-toggle
[(mModel)]="interval"
leftValue="yearly"
rightValue="monthly"
></m-toggle
></span>
<span>Monthly</span>
</div>
<div class="m-upgradesUpgradeOptionsHeader__toggle">
<span>USD</span>
<span
><m-toggle
[(mModel)]="currency"
leftValue="usd"
rightValue="tokens"
></m-toggle
></span>
<span>Tokens</span>
</div>
</div>
</div>
<div class="m-upgradesUpgradeOptions__plans">
<div
class="m-upgradesUpgradeOptionsPlan__row m-upgradesUpgradeOptionsPlan__row--first"
>
<h3 i18n>Plus</h3>
<p class="m-upgradesUpgradeOptionsPlan__targetAudience">
Ideal for all Minds Users
</p>
</div>
<div class="m-upgradesUpgradeOptionsPlan__row">
<p class="m-upgradesUpgradeOptionsPlan__description">
Upgrade your channel to unlock new features and upgrade your experience.
</p>
</div>
<div class="m-upgradesUpgradeOptionsPlan__row">
<ul>
<li>Access exclusive content</li>
<li>Channel verification</li>
<li>Hide boosted content</li>
</ul>
<p>
<a
class="m-upgradesUpgradeOptionsPlan__moreInfo"
routerLink="/plus"
[queryParams]="intervalCurrencyQueryParams"
>
More info
</a>
</p>
</div>
<div class="m-upgradesUpgradeOptionsPlan__row">
<p class="m-upgradesUpgradeOptionsPlan__pricing">
<span>{{ plusPricing.amount }}</span> per month
</p>
<p
class="m-upgradesUpgradeOptionsPlan__offer"
*ngIf="plusPricing.offerFrom"
>
{{ plusPricing.offerFrom }} per month
</p>
</div>
<div class="m-upgradesUpgradeOptionsPlan__row">
<a
class="mf-button m-upgradesUpgradeOptionsPlan__action"
routerLink="/plus"
[queryParams]="intervalCurrencyQueryParams"
>
Upgrade to Plus
</a>
</div>
<div
class="m-upgradesUpgradeOptionsPlan__row m-upgradesUpgradeOptionsPlan__row--first"
>
<h3>Pro</h3>
<p class="m-upgradesUpgradeOptionsPlan__targetAudience">
For independent content creators
</p>
</div>
<div class="m-upgradesUpgradeOptionsPlan__row">
<p class="m-upgradesUpgradeOptionsPlan__description">
The ultimate platform for independent content creators on the Internet.
</p>
</div>
<div class="m-upgradesUpgradeOptionsPlan__row">
<ul>
<li>Get paid for your work</li>
<li>Build your own custom website</li>
<li>Professional media tools</li>
</ul>
<p>
<a
class="m-upgradesUpgradeOptionsPlan__moreInfo"
routerLink="/pro"
[queryParams]="intervalCurrencyQueryParams"
>
More info
</a>
</p>
</div>
<div class="m-upgradesUpgradeOptionsPlan__row">
<p class="m-upgradesUpgradeOptionsPlan__pricing">
<span>{{ proPricing.amount }}</span> per month
</p>
<p
class="m-upgradesUpgradeOptionsPlan__offer"
*ngIf="proPricing.offerFrom"
>
{{ proPricing.offerFrom }} per month
</p>
</div>
<div class="m-upgradesUpgradeOptionsPlan__row">
<a
class="mf-button m-upgradesUpgradeOptionsPlan__action"
routerLink="/pro"
[queryParams]="intervalCurrencyQueryParams"
>
Upgrade to Pro
</a>
</div>
<div
class="m-upgradesUpgradeOptionsPlan__row m-upgradesUpgradeOptionsPlan__row--first"
>
<h3>Node</h3>
<p class="m-upgradesUpgradeOptionsPlan__targetAudience">
For next-level content creators
</p>
</div>
<div class="m-upgradesUpgradeOptionsPlan__row">
<p class="m-upgradesUpgradeOptionsPlan__description">
Launch your own white-labeled social network and mobile app.
</p>
</div>
<div class="m-upgradesUpgradeOptionsPlan__row">
<ul>
<li>White-labeled mobile app</li>
<li>Custom domain</li>
<li>Personalized branding</li>
</ul>
<p>
<a class="m-upgradesUpgradeOptionsPlan__moreInfo" routerLink="/nodes">
More info
</a>
</p>
</div>
<div class="m-upgradesUpgradeOptionsPlan__row">
<p class="m-upgradesUpgradeOptionsPlan__pricing">
Contact us for pricing
</p>
</div>
<div class="m-upgradesUpgradeOptionsPlan__row">
<a
class="mf-button m-upgradesUpgradeOptionsPlan__action"
routerLink="/nodes"
>
Contact us
</a>
</div>
</div>
</div>
@import '../../foundation/grid-values';
m-upgrades__upgradeOptions {
display: block;
}
.m-upgrades__upgradeOptions {
position: relative; /* required for deco */
z-index: 0; /* required for deco */
.m-upgradesUpgradeOptions__header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
@media screen and (max-width: $m-grid-min-vp) {
display: block;
margin: 0 40px 40px;
}
h2 {
font-size: 32px;
font-weight: 900;
line-height: 43px;
@media screen and (max-width: $m-grid-min-vp) {
font-size: 24px;
margin: 0 0 20px;
}
}
.m-upgradesUpgradeOptionsHeader__toggleContainer {
display: flex;
flex-direction: row;
align-items: center;
.m-upgradesUpgradeOptionsHeader__toggle {
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: stretch;
margin-right: 50px;
&:last-child {
margin-right: 0;
}
}
}
}
.m-upgradesUpgradeOptions__plans {
position: relative; /* required for deco, no z-indexes HERE! */
display: grid;
grid-auto-flow: column;
grid-column-gap: 65px;
grid-row-gap: 0;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(5, auto);
padding: 40px 60px 60px;
@include m-theme() {
background-color: lighten(themed($m-grey-50), 5%);
}
@media screen and (max-width: $m-grid-min-vp) {
display: block;
margin: 0 20px;
padding: 40px;
.m-upgradesUpgradeOptionsPlan__row.m-upgradesUpgradeOptionsPlan__row--first {
margin-top: 40px;
&:first-child {
margin-top: 0;
}
}
}
// Deco
&::before {
content: '';
display: block;
position: absolute;
width: 393px;
height: 193px;
top: 0;
left: 0;
transform: translate(-20px, -20px);
background: url('<%= APP_CDN %>/assets/marketing/deco_1.svg');
z-index: -1;
@media screen and (max-width: $m-grid-min-vp) {
max-width: 90vw;
}
}
&::after {
content: '';
display: block;
position: absolute;
bottom: 0;
right: 0;
width: 388px;
height: 388px;
background: url('<%= APP_CDN %>/assets/marketing/deco_2.svg');
z-index: -1;
transform: translate(102px, 102px);
@media screen and (max-width: $m-grid-min-vp) {
display: none;
}
}
//
h3 {
font-size: 28px;
font-weight: 900;
line-height: 37px;
padding: 0 0 2px;
margin: 0;
}
p {
font-size: 16px;
line-height: 21px;
margin: 0 0 31px;
&.m-upgradesUpgradeOptionsPlan__targetAudience {
@include m-theme() {
color: themed($m-green);
}
}
&.m-upgradesUpgradeOptionsPlan__description {
@include m-theme() {
color: themed($m-grey-600);
}
}
}
ul {
padding: 0 12px;
margin: 0 0 16px;
li {
font-size: 16px;
margin-bottom: 12px;
}
}
.m-upgradesUpgradeOptionsPlan__moreInfo {
font-size: 14px;
line-height: 19px;
cursor: pointer;
@include m-theme() {
color: themed($m-blue);
}
}
.m-upgradesUpgradeOptionsPlan__pricing {
font-size: 18px;
font-weight: bold;
line-height: 24px;
margin-bottom: 0;
@include m-theme() {
color: themed($m-grey-600);
}
span {
font-size: 24px;
@include m-theme() {
color: themed($m-black);
}
}
}
.m-upgradesUpgradeOptionsPlan__offer {
font-size: 14px;
text-decoration: line-through;
margin-bottom: 0;
@include m-theme() {
color: themed($m-red);
}
}
.m-upgradesUpgradeOptionsPlan__action {
margin-top: 25px;
}
}
}
import { ChangeDetectionStrategy, Component } from '@angular/core';
import currency, { Currency } from '../../helpers/currency';
export type UpgradeOptionInterval = 'yearly' | 'monthly';
export type UpgradeOptionCurrency = Currency;
@Component({
selector: 'm-upgrades__upgradeOptions',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'upgrade-options.component.html',
})
export class UpgradeOptionsComponent {
minds = window.Minds;
interval: UpgradeOptionInterval = 'yearly';
currency: UpgradeOptionCurrency = 'usd';
get intervalCurrencyQueryParams() {
return { i: this.interval, c: this.currency };
}
get plusPricing() {
if (this.interval === 'yearly') {
return {
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(
this.minds.upgrades.plus.monthly[this.currency],
this.currency
),
offerFrom: null,
};
}
}
get proPricing() {
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,
};
}
}
}
<m-marketing
class="m-upgrades"
pageTitle="Upgrade your Minds experience"
i18n-pageTitle
>
<div class="m-marketing__main m-marketing__section--style-1">
<div class="m-grid m-marketing__wrapper">
<div class="m-grid__column-5 m-grid__column-12--mobile m-upgrades__body">
<h2 class="m-marketing__subtitle--asTitle" i18n>
Upgrade your Minds experience
</h2>
<p class="m-marketing__description" i18n>
Minds offers a unique range of powerful upgrades that will supercharge
your Minds experience.
</p>
<ul class="m-marketing__points">
<li>Join Plus to hide boosts, access exclusive content and more</li>
<li>
Launch Pro to build your own website and get paid for your work
</li>
<li>Buy Minds Tokens to boost content and support creators</li>
</ul>
<a
class="mf-button mf-button--hollow m-marketing--hideMobile"
(click)="upgradeNow(); $event.preventDefault()"
i18n
>
Upgrade now
</a>
</div>
<div class="m-grid__column-7 m-grid__column-12--mobile m-upgrades__image">
<!-- Image -->
</div>
</div>
</div>
<m-marketing__asFeaturedIn slot="2"></m-marketing__asFeaturedIn>
<div class="m-marketing__extras">
<div class="mf-jumpAnchor" #upgradeOptionsAnchor></div>
<m-upgrades__upgradeOptions></m-upgrades__upgradeOptions>
</div>
<div class="m-marketing__extras">
<m-upgrades__buyTokens></m-upgrades__buyTokens>
</div>
</m-marketing>
.m-upgrades {
.m-upgrades__featuredIn {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
list-style: none;
margin: 0;
padding: 0;
li {
font-size: 14px;
@include m-theme() {
color: themed($m-grey-200);
}
img {
width: 55px;
height: 55px;
object-fit: contain;
}
&.m-upgrades__featuredInItem--big {
img {
width: 96px;
height: 96px;
}
}
}
}
m-upgrades__upgradeOptions {
margin-bottom: 110px;
@media screen and (max-width: $m-grid-min-vp) {
margin-bottom: 40px;
}
}
}
import {
ChangeDetectionStrategy,
Component,
ElementRef,
ViewChild,
} from '@angular/core';
@Component({
selector: 'm-upgrades',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'upgrades.component.html',
})
export class UpgradesComponent {
@ViewChild('upgradeOptionsAnchor', { static: false })
readonly upgradeOptionsAnchor: ElementRef;
upgradeNow() {
if (this.upgradeOptionsAnchor.nativeElement) {
this.upgradeOptionsAnchor.nativeElement.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest',
});
}
}
}
import { NgModule } from '@angular/core';
import { CommonModule as NgCommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { CommonModule } from '../../common/common.module';
import { UpgradesComponent } from './upgrades.component';
import { UpgradeOptionsComponent } from './upgrade-options.component';
import { BuyTokensComponent } from './buy-tokens.component';
export const routes = [
{ path: 'upgrade', pathMatch: 'full', redirectTo: 'upgrades' },
{ path: 'upgrades', component: UpgradesComponent },
];
@NgModule({
imports: [
RouterModule.forChild(routes),
NgCommonModule,
FormsModule,
ReactiveFormsModule,
CommonModule,
],
declarations: [
UpgradesComponent,
UpgradeOptionsComponent,
BuyTokensComponent,
],
})
export class UpgradesModule {}
......@@ -162,6 +162,11 @@
outline: none;
}
}
.m-wire--creator-wide-input--fixedAmount {
margin-right: 0.35em;
font-weight: 700;
}
}
.m-wire--creator-selector {
......
<div class="m-wire--creator">
<div class="m-wire--creator--header">
<h2>{{ owner.name }}</h2>
</div>
<section class="m-wire--creator-section-row">
<!-- Amount -->
<section class="m-wire--creator-section">
<!-- Amount -->
<h3 class="m-wire--creator-section-title--small">
You're paying
</h3>
<div
class="m-wire--creator-wide-input m-wire--creator--amount"
[class.m-wire--creator-wide-input--reading]="!editingAmount"
>
<div class="m-wire--creator-wide-input--fixedAmount">
{{ wire.amount }}
</div>
<div
class="m-wire--creator-wide-input--label"
(click)="amountEditorFocus()"
>
<span
i18n="tokens input|@@M__COMMON__TOKENS_INPUT"
*ngIf="
wire.payloadType === 'onchain' || wire.payloadType === 'offchain'
"
>Tokens</span
>
<span
i18n="tokens input|@@M__COMMON__TOKENS_INPUT"
*ngIf="wire.payloadType === 'usd'"
>USD</span
>
<span
i18n="tokens input|@@M__COMMON__TOKENS_INPUT"
*ngIf="wire.payloadType === 'eth'"
>ETH</span
>
<span
i18n="tokens input|@@M__COMMON__TOKENS_INPUT"
*ngIf="wire.payloadType === 'btc'"
>BTC</span
>
<span
i18n="tokens input|@@M__COMMON__TOKENS_INPUT"
*ngIf="wire.payloadType === 'erc20'"
>ERC-20</span
>
<ul *ngIf="false">
<li>USD</li>
<li>ETH</li>
<li>ERC-20</li>
</ul>
</div>
</div>
<div class="m-wire--creator-wide-input--cost" hidden>
<span class="m-wire--creator-wide-input--cost-label-value">
<span class="m-wire--creator-wide-input--cost-label">Cost</span>
<span class="m-wire--creator-wide-input--cost-value">
<span *ngIf="tokenRate">{{
getTokenAmountRate(wire.amount) | currency: 'USD':'symbol-narrow'
}}</span>
<span *ngIf="!tokenRate">&hellip;</span>
</span>
</span>
<span
class="m-wire--creator-wide-input--cost-currency"
i18n="@@M__COMMON__USD_LABEL"
>
USD
</span>
</div>
<div class="m-wire--creator--recurring" *ngIf="canRecur && false">
<label
class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect"
for="wire-recurring-1"
[mdlSwitch]
[toggled]="wire.recurring"
>
<input
type="checkbox"
id="wire-recurring-1"
class="mdl-checkbox__input"
[ngModel]="wire.recurring"
(ngModelChange)="toggleRecurring()"
/>
<span
class="mdl-checkbox__label"
i18n="@@WIRE__CREATOR__REPEAT_TRANSACTION_MONTHLY"
>Repeat this transaction monthly.</span
>
</label>
</div>
</section>
<!-- Payment Method -->
<section class="m-wire--creator-section m-wire--creator-payment-section">
<!-- Credit card selector -->
<ng-container *ngIf="wire.payloadType === 'usd'">
<h3 class="m-wire--creator-section-title--small">Select or add card</h3>
<m-payments__selectCard
*ngIf="wire.payloadType === 'usd'"
(selected)="setUsdPaymentMethod($event)"
>
</m-payments__selectCard>
</ng-container>
<div
class="m-wireCreator__tokenMethod"
*ngIf="
wire.payloadType === 'onchain' || wire.payloadType === 'offchain'
"
>
<h3 class="m-wire--creator-section-title--small">Select a wallet</h3>
<div class="m-selector">
<select
[ngModel]="wire.payloadType"
(ngModelChange)="setPayloadType($event)"
>
<option value="onchain">Onchain / Web3 (Metamask)</option>
<option value="offchain">Offchain</option>
</select>
</div>
</div>
</section>
</section>
<!-- Submit -->
<section
class="m-wire--creator-section m-wire--creator-section--last"
(mouseenter)="showErrors()"
>
<div class="m-wire--creator--submit">
<button
class="m-wire--creator-button m-btn m-btn--action m-btn--slim"
[disabled]="!canSubmit() || inProgress || success || criticalError"
(click)="submit()"
i18n="@@WIRE__CREATOR__SEND_ACTION"
>
SEND
</button>
<div
*ngIf="inProgress"
class="m-wire--creator--submit-label mdl-spinner mdl-js-spinner is-active"
[mdl]
></div>
<div *ngIf="!inProgress && success" class="m-wire--creator--submit-label">
<i class="material-icons">check</i>
<span i18n="@@WIRE__CREATOR__WIRED_NOTICE">Wired!</span>
</div>
<div *ngIf="!inProgress && !!error" class="m-wire--creator--submit-error">
<i class="material-icons">close</i>
<span>{{ error }}</span>
</div>
</div>
</section>
</div>
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, Input } 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 {
@Input('opts') set opts(opts) {
this._opts = opts;
this.wire.amount = opts.amount;
switch (opts.currency) {
case 'tokens':
this.wire.payloadType = 'offchain';
break;
default:
this.wire.payloadType = opts.currency;
}
}
ngOnInit() {}
}
<div class="m-wire-payment--creator">
<section class="m-wire--creator-section-row">
<!-- Amount -->
<section class="m-wire--creator-section">
<h3
i18n="@@WIRE__CREATOR__AMOUNT_TITLE_HOW_MANY_TOKENS"
style="font-size: 36px; text-transform: none;"
>
Send {{ wire.amount }} Tokens to Minds Plus
</h3>
<div class="m-wire--creator--recurring">
<label
class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect"
for="wire-recurring-1"
[mdlSwitch]
[toggled]="wire.recurring"
>
<input
type="checkbox"
id="wire-recurring-1"
class="mdl-checkbox__input"
[ngModel]="wire.recurring"
(ngModelChange)="toggleRecurring()"
/>
<span
class="mdl-checkbox__label"
i18n="@@WIRE__CREATOR__REPEAT_TRANSACTION_MONTHLY"
>Repeat this transaction {{ wire.period }}ly.</span
>
</label>
</div>
<div class="m-wire--creator--submit" style="margin-top:24px">
<button
class="m-wire--creator-button"
[disabled]="!canSubmit() || inProgress || success || criticalError"
(click)="submit()"
i18n="@@WIRE__CREATOR__SEND_ACTION"
>
SEND
</button>
<div
*ngIf="inProgress"
class="m-wire--creator--submit-label mdl-spinner mdl-js-spinner is-active"
[mdl]
></div>
<div
*ngIf="!inProgress && success"
class="m-wire--creator--submit-label"
>
<i class="material-icons">check</i>
<span i18n="@@WIRE__CREATOR__WIRED_NOTICE">Wired!</span>
</div>
<div
*ngIf="!inProgress && !!error"
class="m-wire--creator--submit-error"
>
<i class="material-icons">close</i>
<span>{{ error }}</span>
</div>
</div>
</section>
<!-- Payment Method -->
<section class="m-wire--creator-section m-wire--creator-payment-section">
<h3
class="m-wire--creator-section-title--small"
i18n="@@WIRE__CREATOR__WIRE_TYPE_TITLE"
>
Payment Method
</h3>
<ul
class="m-wire--creator-selector"
[class.m-wire--creator-selector--has-selection]="wire.payloadType"
>
<li
(click)="setPayloadType('onchain')"
[class.m-wire--creator-selector--highlight]="
!wire.payloadType || wire.payloadType === 'onchain'
"
>
<div class="m-wire--creator-selector-type">
<i class="material-icons">check_circle</i>
<h5>
<span i18n="@@M__COMMON__ONCHAIN">OnChain</span>
<m-tooltip icon="help" i18n="@@WIRE__CREATOR__ONCHAIN_TOOLTIP">
Onchain payments will be published to the public blockchain. You
may utilize Metamask or another address. These purchases require
a Gas fee.
</m-tooltip>
</h5>
<span class="m-wire--creator-selector-subtext">
<m-tooltip anchor="left">
<span
m-tooltip--anchor
class="m-boost--creator-selector--hoverable"
i18n="@@M__COMMON__BALANCE"
>Balance</span
>
<ng-container
*ngIf="
!balances.isReceiverOnchain;
else receiverOnchainTooltip
"
i18n="@@WIRE__CREATOR__ACTIVE_ADDRESS_TOOLTIP"
>
This balance is for the
{{ balances.onChainAddress | addressExcerpt: true }} wallet
that is currently active in your Ethereum interface.
</ng-container>
<ng-template
#receiverOnchainTooltip
i18n="@@WIRE__CREATOR__RECEIVER_ADDRESS_TOOLTIP"
>
This balance is for the
{{ balances.onChainAddress | addressExcerpt: true }} wallet
that is currently set up as the receiver wallet for your
channel.
</ng-template> </m-tooltip
>:
<ng-container
*ngIf="balances.onchain !== null; else loadingOnchainBalance"
>
{{ balances.onchain | token: 18 | number }}
</ng-container>
<ng-template #loadingOnchainBalance>&hellip;</ng-template>
</span>
</div>
<span
class="m-wire--creator-selector--selected-label"
i18n="@@M__COMMON__SELECTED"
>Selected</span
>
</li>
<li
(click)="setPayloadType('offchain')"
[class.m-wire--creator-selector--highlight]="
!wire.payloadType || wire.payloadType === 'offchain'
"
>
<div class="m-wire--creator-selector-type">
<i class="material-icons">donut_large</i>
<h5>
<span i18n="@@M__COMMON__OFFCHAIN">OffChain</span>
<m-tooltip icon="help" i18n="@@WIRE__CREATOR__OFFCHAIN_TOOLTIP">
Offchain payments will not be published to the blockchain and
are limited to 100 tokens a day.
</m-tooltip>
</h5>
<span class="m-wire--creator-selector-subtext">
<m-tooltip anchor="left">
<span
m-tooltip--anchor
class="m-boost--creator-selector--hoverable"
i18n="@@M__COMMON__BALANCE"
>Balance</span
>
<ng-container i18n="@@WIRE__CREATOR__BALANCE_OFFCHAIN_TOOLTIP">
This balance is for your Minds.com OffChain wallet.
Transactions made from this address will not appear on the
blockchain.
</ng-container> </m-tooltip
>:
<ng-container
*ngIf="balances.onchain !== null; else loadingOnchainBalance"
>
{{ balances.offchain | token: 18 | number }}
</ng-container>
<ng-template #loadingOnchainBalance>&hellip;</ng-template>
</span>
</div>
<span
class="m-wire--creator-selector--selected-label"
i18n="@@M__COMMON__SELECTED"
>Selected</span
>
</li>
<li (click)="buyTokens()">
<div class="m-wire--creator-selector-type">
<i class="material-icons">monetization_on</i>
<h5 i18n="@@M__COMMON__BUY_MINDS_TOKENS">
<span>Buy Tokens</span>
</h5>
<span
class="m-wire--creator-selector-subtext"
i18n="@@M__WIRE_CREATOR__DONT_HAVE_TOKENS"
>
Don't have tokens?
</span>
</div>
<span
class="m-wire--creator-selector--selected-label"
i18n="@@M__COMMON__SELECTED"
>Selected</span
>
</li>
<ng-template #spacer>
<li class="m-layout--spacer"></li>
</ng-template>
</ul>
</section>
</section>
</div>
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,
......
src/assets/marketing/bbc.png

8.95 KB

src/assets/marketing/buytokens_bg.jpg

51.7 KB

This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
src/assets/marketing/forbes.png

5.32 KB

This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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