mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-25 09:03:12 +03:00
Removed all launch-wizard related code
No issue - With the onboarding flow redesign, the launch wizard can no longer be accessed and is therefore deleted.
This commit is contained in:
parent
0b8b530efb
commit
594e2ccb08
@ -1,123 +0,0 @@
|
||||
<div class="gh-launch-wizard-settings-container">
|
||||
<div class="gh-stack overflow-y-auto flex-grow-1">
|
||||
<div class="gh-stack-item gh-setting-first">
|
||||
<div class="gh-members-stripe-info gh-launch-wizard-stripe-info">
|
||||
<div class="gh-members-stripe-info-header">
|
||||
<h4>Getting paid</h4>
|
||||
{{svg-jar "stripe-verified-partner-badge" class="gh-members-stripe-badge"}}
|
||||
</div>
|
||||
<p class="f8 mt2 mb0">
|
||||
Stripe is our exclusive direct payments partner. Ghost collects <strong>no fees</strong> on any payments! If you don’t have a Stripe account yet, you can <a href="https://stripe.com" target="_blank" rel="noopener noreferrer" class="gh-members-stripe-link">sign up here</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.config.stripeDirect}}
|
||||
<div class="gh-stack-item gh-setting flex-column">
|
||||
<div class="mb4">
|
||||
<label for="stripe-publishable-key" class="gh-setting-title">Stripe Publishable key</label>
|
||||
<GhTextInput
|
||||
@id="stripe-publishable-key"
|
||||
@type="password"
|
||||
@value={{readonly this.settings.stripePublishableKey}}
|
||||
class="mt1 password"
|
||||
{{on "input" this.setStripeDirectPublicKey}}
|
||||
/>
|
||||
{{#if this.stripePublishableKeyError}}<p class="mb0 mt2 f8 red">{{this.stripePublishableKeyError}}</p>{{/if}}
|
||||
</div>
|
||||
<div>
|
||||
<label for="stripe-secret-key" class="gh-setting-title">Stripe Secret key</label>
|
||||
<GhTextInput
|
||||
@id="stripe-secret-key"
|
||||
@type="password"
|
||||
@value={{readonly this.settings.stripeSecretKey}}
|
||||
class="mt1 password"
|
||||
{{on "input" this.setStripeDirectSecretKey}}
|
||||
/>
|
||||
{{#if this.stripeSecretKeyError}}<p class="mb0 mt2 f8 red">{{this.stripeSecretKeyError}}</p>{{/if}}
|
||||
<a href="https://dashboard.stripe.com/account/apikeys" target="_blank" rel="noopener noreferrer" class="mt1 fw4 f8">
|
||||
Find your Stripe API keys here »
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-setting-desc"><a href="javascript:void(0)" {{on "click" @skipStep}}>Skip</a> if you don't want to offer paid subscriptions.</div>
|
||||
{{else}}
|
||||
<div class="gh-stack-item gh-setting flex-wrap">
|
||||
{{!-- Stripe already configured --}}
|
||||
{{#if this.settings.stripeConnectAccountId}}
|
||||
<div>
|
||||
<h4 class="gh-setting-title">Already connected to Stripe</h4>
|
||||
<p class="gh-setting-desc mt2">
|
||||
Connected to <a href="https://dashboard.stripe.com/{{this.settings.stripeConnectAccountId}}" target="_blank" rel="noopener noreferrer">{{this.settings.stripeConnectDisplayName}}</a>
|
||||
|
||||
{{#unless this.settings.stripeConnectLivemode}}
|
||||
<span class="gh-members-connect-testmodelabel">Test mode</span>
|
||||
{{/unless}}
|
||||
</p>
|
||||
</div>
|
||||
{{#if this.hasActiveStripeSubscriptions}}
|
||||
<p class="red ma0 pa0 f8 nudge-bottom--2">
|
||||
Cannot disconnect while there are members with active Stripe subscriptions.
|
||||
</p>
|
||||
{{else}}
|
||||
<div class="gh-setting-action">
|
||||
<button type="button" class="gh-btn" {{on "click" (perform this.openDisconnectStripeConnectModalTask)}}><span>Disconnect</span></button>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{!-- Stripe not yet configured --}}
|
||||
{{else}}
|
||||
<div class="w-100">
|
||||
<div class="gh-setting-title">Generate secure key</div>
|
||||
<div class="flex items-center mb4 gh-members-connectbutton-container justify-between mt2">
|
||||
<a href="{{this.stripeConnectAuthUrl}}" class="stripe-connect" target="_blank" rel="noopener noreferrer"><span>Connect with Stripe</span></a>
|
||||
<div class="ml2 flex items-center flex-nowrap">
|
||||
<span class="mr2 f8 midgrey nowrap {{if this.stripeConnectTestMode "gh-members-connect-testmodeon"}}">Test mode</span>
|
||||
<div class="for-switch small">
|
||||
<label class="switch" for="stripe-connect-test-mode" {{on "click" this.toggleStripeConnectTestMode}}>
|
||||
<input type="checkbox" class="gh-input" checked={{this.stripeConnectTestMode}} {{on "click" this.toggleStripeConnectTestMode}} data-test-checkbox="stripe-connect-test-mode">
|
||||
<span class="input-toggle-component mt1"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-setting-action">
|
||||
<GhTextarea
|
||||
class="gh-launch-wizard-stripe-connect-token"
|
||||
placeholder="Paste your secure key here"
|
||||
{{on "input" this.setStripeConnectIntegrationToken}}
|
||||
/>
|
||||
{{#if this.stripeConnectError}}<p class="mb0 mt2 f8 red">{{this.stripeConnectError}}</p>{{/if}}
|
||||
</div>
|
||||
<div class="gh-setting-desc skip-step"><a href="javascript:void(0)" {{on "click" @skipStep}}>Skip</a> if you don't want to offer paid subscriptions.</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-launch-wizard-nav-buttons">
|
||||
<button type="button" class="gh-btn gh-btn-outline gh-btn-icon-dark gh-btn-large w-30" {{on "click" @backStep}}><span>{{svg-jar "arrow-left-tail"}}</span></button>
|
||||
|
||||
<GhTaskButton
|
||||
@task={{this.saveAndContinueTask}}
|
||||
@runningText="Saving"
|
||||
@class="w-70 ml4 right gh-btn gh-btn-black gh-btn-large gh-btn-icon-right"
|
||||
data-test-button="wizard-next"
|
||||
>
|
||||
{{#if this.saveAndContinueTask.isRunning}}
|
||||
<span>Saving...</span>
|
||||
{{else}}
|
||||
<span>{{if this.settings.stripeConnectAccountId "Continue" "Save and continue"}}{{svg-jar "arrow-right-tail"}}</span>
|
||||
{{/if}}
|
||||
</GhTaskButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.showDisconnectStripeConnectModal}}
|
||||
<GhFullscreenModal
|
||||
@modal="disconnect-stripe"
|
||||
@model={{hash
|
||||
stripeConnectAccountName=this.settings.stripeConnectDisplayName
|
||||
}}
|
||||
@confirm={{perform this.disconnectStripeConnectIntegrationTask}}
|
||||
@close={{this.closeDisconnectStripeModal}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
@ -1,203 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task, timeout} from 'ember-concurrency';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
const RETRY_PRODUCT_SAVE_POLL_LENGTH = 1000;
|
||||
const RETRY_PRODUCT_SAVE_MAX_POLL = 15 * RETRY_PRODUCT_SAVE_POLL_LENGTH;
|
||||
|
||||
export default class GhLaunchWizardConnectStripeComponent extends Component {
|
||||
@service ajax;
|
||||
@service config;
|
||||
@service ghostPaths;
|
||||
@service settings;
|
||||
@service store;
|
||||
|
||||
@tracked hasActiveStripeSubscriptions = false;
|
||||
@tracked showDisconnectStripeConnectModal = false;
|
||||
@tracked stripeConnectTestMode = false;
|
||||
@tracked stripeConnectError = null;
|
||||
@tracked stripePublishableKeyError = null;
|
||||
@tracked stripeSecretKeyError = null;
|
||||
|
||||
get stripeConnectAuthUrl() {
|
||||
const mode = this.stripeConnectTestMode ? 'test' : 'live';
|
||||
return `${this.ghostPaths.url.api('members/stripe_connect')}?mode=${mode}`;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.args.updatePreview('');
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy?.(...arguments);
|
||||
// clear any unsaved settings changes when going back/forward/closing
|
||||
this.settings.rollbackAttributes();
|
||||
}
|
||||
|
||||
@action
|
||||
setStripeDirectPublicKey(event) {
|
||||
this.settings.set('stripeProductName', this.settings.get('title'));
|
||||
this.settings.set('stripePublishableKey', event.target.value);
|
||||
this.stripePublishableKeyError = null;
|
||||
}
|
||||
|
||||
@action
|
||||
setStripeDirectSecretKey(event) {
|
||||
this.settings.set('stripeProductName', this.settings.get('title'));
|
||||
this.settings.set('stripeSecretKey', event.target.value);
|
||||
this.stripeSecretKeyError = null;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleStripeConnectTestMode() {
|
||||
this.stripeConnectTestMode = !this.stripeConnectTestMode;
|
||||
}
|
||||
|
||||
@action
|
||||
setStripeConnectIntegrationToken(event) {
|
||||
this.settings.set('stripeProductName', this.settings.get('title'));
|
||||
this.settings.set('stripeConnectIntegrationToken', event.target.value);
|
||||
this.stripeConnectError = null;
|
||||
}
|
||||
|
||||
calculateDiscount(monthly, yearly) {
|
||||
if (isNaN(monthly) || isNaN(yearly)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return monthly ? 100 - Math.floor((yearly / 12 * 100) / monthly) : 0;
|
||||
}
|
||||
|
||||
getActivePrice(prices, interval, amount, currency) {
|
||||
return prices.find((price) => {
|
||||
return (
|
||||
price.active && price.amount === amount && price.type === 'recurring' &&
|
||||
price.interval === interval && price.currency.toLowerCase() === currency.toLowerCase()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*saveTier() {
|
||||
let pollTimeout = 0;
|
||||
while (pollTimeout < RETRY_PRODUCT_SAVE_MAX_POLL) {
|
||||
yield timeout(RETRY_PRODUCT_SAVE_POLL_LENGTH);
|
||||
|
||||
try {
|
||||
const updatedTier = yield this.tier.save();
|
||||
|
||||
yield this.settings.save();
|
||||
|
||||
return updatedTier;
|
||||
} catch (error) {
|
||||
if (error.payload?.errors && error.payload.errors[0].code === 'STRIPE_NOT_CONFIGURED') {
|
||||
pollTimeout += RETRY_PRODUCT_SAVE_POLL_LENGTH;
|
||||
// no-op: will try saving again as stripe is not ready
|
||||
continue;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.tier;
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*openDisconnectStripeConnectModalTask() {
|
||||
this.hasActiveStripeSubscriptions = false;
|
||||
|
||||
const url = this.ghostPaths.url.api('/members/') + '?filter=status:paid&limit=0';
|
||||
const response = yield this.ajax.request(url);
|
||||
|
||||
if (response?.meta?.pagination?.total !== 0) {
|
||||
this.hasActiveStripeSubscriptions = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.showDisconnectStripeConnectModal = true;
|
||||
}
|
||||
|
||||
@action
|
||||
closeDisconnectStripeModal() {
|
||||
this.showDisconnectStripeConnectModal = false;
|
||||
}
|
||||
|
||||
@task
|
||||
*disconnectStripeConnectIntegrationTask() {
|
||||
this.disconnectStripeError = false;
|
||||
const url = this.ghostPaths.url.api('/settings/stripe/connect');
|
||||
|
||||
yield this.ajax.delete(url);
|
||||
yield this.settings.reload();
|
||||
}
|
||||
|
||||
@task
|
||||
*saveAndContinueTask() {
|
||||
if (this.config.get('stripeDirect')) {
|
||||
if (!this.settings.get('stripePublishableKey')) {
|
||||
this.stripePublishableKeyError = 'Enter your publishable key to continue';
|
||||
}
|
||||
|
||||
if (!this.settings.get('stripeSecretKey')) {
|
||||
this.stripeSecretKeyError = 'Enter your secret key to continue';
|
||||
}
|
||||
|
||||
if (this.stripePublishableKeyError || this.stripeSecretKeyError) {
|
||||
return false;
|
||||
}
|
||||
} else if (!this.settings.get('stripeConnectAccountId') && !this.settings.get('stripeConnectIntegrationToken')) {
|
||||
this.stripeConnectError = 'Paste your secure key to continue';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.config.get('stripeDirect') && this.settings.get('stripeConnectAccountId')) {
|
||||
this.args.nextStep();
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
yield this.settings.save();
|
||||
|
||||
const tiers = yield this.store.query('tier', {filter: 'type:paid', include: 'monthly_price,yearly_price'});
|
||||
this.tier = tiers.firstObject;
|
||||
|
||||
if (this.tier) {
|
||||
this.tier.set('currency', 'usd');
|
||||
this.tier.set('monthlyPrice', 500);
|
||||
this.tier.set('yearlyPrice', 5000);
|
||||
yield this.saveTier.perform();
|
||||
this.settings.set('portalPlans', ['free', 'monthly', 'yearly']);
|
||||
yield this.settings.save();
|
||||
}
|
||||
|
||||
this.pauseAndContinueTask.perform();
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.payload?.errors && error.payload.errors[0].type === 'ValidationError') {
|
||||
const [validationError] = error.payload.errors;
|
||||
|
||||
if (this.config.get('stripeDirect')) {
|
||||
if (validationError.context.match(/stripe_publishable_key/)) {
|
||||
this.stripePublishableKeyError = 'Invalid publishable key';
|
||||
} else {
|
||||
this.stripeSecretKeyError = 'Invalid secret key';
|
||||
}
|
||||
} else {
|
||||
this.stripeConnectError = 'Invalid secure key';
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
*pauseAndContinueTask() {
|
||||
this.args.refreshPreview();
|
||||
yield timeout(500);
|
||||
this.args.nextStep();
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
<div class="gh-branding-settings">
|
||||
<section class="gh-launch-wizard-settings-container">
|
||||
<GhBrandSettingsForm
|
||||
class="overflow-y-auto flex-grow-1"
|
||||
@replacePreviewContents={{@replacePreviewContents}}
|
||||
/>
|
||||
|
||||
<div class="gh-launch-wizard-nav-buttons">
|
||||
<GhTaskButton
|
||||
@task={{this.saveAndContinueTask}}
|
||||
@buttonText={{html-safe (concat "Save and continue " (svg-jar "arrow-right-tail"))}}
|
||||
type="button"
|
||||
class="gh-btn gh-btn-black gh-btn-icon-right gh-btn-large gh-launch-wizard-btn w-100"
|
||||
data-test-button="wizard-next"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
@ -1,30 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default class GhLaunchWizardCustomiseDesignComponent extends Component {
|
||||
@service notifications;
|
||||
@service settings;
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy?.(...arguments);
|
||||
this.settings.rollbackAttributes();
|
||||
this.settings.errors.remove('accentColor');
|
||||
}
|
||||
|
||||
@task
|
||||
*saveAndContinueTask() {
|
||||
try {
|
||||
if (this.settings.errors && this.settings.errors.length !== 0) {
|
||||
return;
|
||||
}
|
||||
yield this.settings.save();
|
||||
this.args.nextStep();
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
this.notifications.showAPIError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<div class="gh-launch-wizard-settings-container">
|
||||
<div class="overflow-auto flex-grow-1">
|
||||
<h4>All looks good?</h4>
|
||||
<p>You are all set up to start creating content, grow an audience and make your first sale!</p>
|
||||
<p>You can further customize your site in Settings.</p>
|
||||
</div>
|
||||
<div class="gh-launch-wizard-nav-buttons">
|
||||
<button type="button" class="gh-btn gh-btn-outline gh-btn-icon-dark gh-btn-large w-30" {{on "click" @backStep}}><span>{{svg-jar "arrow-left-tail"}}</span></button>
|
||||
<GhTaskButton
|
||||
@task={{this.finaliseTask}}
|
||||
@buttonText="Launch your site!"
|
||||
@runningText="Launching..."
|
||||
@class="w-70 ml4 gh-btn gh-btn-black gh-btn-large gh-btn-icon gh-launch-wizard-btn"
|
||||
data-test-button="wizard-finish"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
@ -1,47 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {htmlSafe} from '@ember/template';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default class GhLaunchWizardFinaliseComponent extends Component {
|
||||
@service feature;
|
||||
@service notifications;
|
||||
@service router;
|
||||
@service settings;
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy?.(...arguments);
|
||||
// clear any unsaved settings changes when going back/forward/closing
|
||||
this.settings.rollbackAttributes();
|
||||
}
|
||||
|
||||
async saveTier() {
|
||||
const data = this.args.getData();
|
||||
this.tier = data?.tier;
|
||||
if (this.tier) {
|
||||
const monthlyAmount = Math.round(data.monthlyAmount * 100);
|
||||
const yearlyAmount = Math.round(data.yearlyAmount * 100);
|
||||
const currency = data.currency;
|
||||
this.tier.set('monthlyPrice', monthlyAmount);
|
||||
this.tier.set('yearlyPrice', yearlyAmount);
|
||||
this.tier.set('currency', currency);
|
||||
const savedTier = await this.tier.save();
|
||||
return savedTier;
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
*finaliseTask() {
|
||||
const data = this.args.getData();
|
||||
if (data?.tier) {
|
||||
yield this.saveTier();
|
||||
this.settings.set('editorIsLaunchComplete', true);
|
||||
yield this.settings.save();
|
||||
}
|
||||
this.router.transitionTo('dashboard');
|
||||
this.notifications.showNotification(
|
||||
'Launch complete!',
|
||||
{type: 'success', actions: htmlSafe('<a href="#/posts">Start creating content</a>')}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
<div class="gh-launch-wizard-settings-container" {{did-insert this.setup}}>
|
||||
{{#if this.isConnectDisallowed}}
|
||||
<div class="gh-stack overflow-y-auto flex-grow-1">
|
||||
<div class="gh-setting-nossl-container">
|
||||
<span class="red">{{svg-jar "shield-lock"}}</span>
|
||||
<h4>Your site is not secured</h4>
|
||||
<p>Paid memberships through Ghost can only be run on sites secured by SSL (HTTPS vs. HTTP). More information on adding a free SSL Certificate to your Ghost site can be <a href="https://ghost.org/integrations/lets-encrypt/" target="_blank" rel="noopener noreferrer">found here</a>.</p>
|
||||
</div>
|
||||
<div class="w-100 mt6">
|
||||
<div class="gh-setting-title">Generate secure key</div>
|
||||
<div class="flex items-center mb4 gh-members-connectbutton-container justify-between mt2">
|
||||
<div class="stripe-connect disabled"><span>Connect with Stripe</span></div>
|
||||
<div class="ml2 flex items-center flex-nowrap">
|
||||
<span class="mr2 f8 midgrey nowrap">Test mode</span>
|
||||
<div class="for-switch small disabled">
|
||||
<label class="switch" for="stripe-connect-test-mode">
|
||||
<input type="checkbox" class="gh-input" disabled="disabled">
|
||||
<span class="input-toggle-component mt1"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-setting-action">
|
||||
<GhTextarea
|
||||
class="gh-launch-wizard-stripe-connect-token"
|
||||
placeholder="Paste your secure key here"
|
||||
disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-stack overflow-y-auto flex-grow-1">
|
||||
<div class="gh-stack-item flex-column">
|
||||
<div class="w-100">
|
||||
<GhFormGroup @class="for-select">
|
||||
<div class="gh-setting-title" for="currency">Plan currency</div>
|
||||
<span class="gh-select mt2">
|
||||
<OneWaySelect
|
||||
@disabled={{this.disabled}}
|
||||
@value={{this.selectedCurrency}}
|
||||
id="currency"
|
||||
name="currency"
|
||||
@options={{readonly this.allCurrencies}}
|
||||
@optionValuePath="value"
|
||||
@optionLabelPath="label"
|
||||
@update={{this.setStripePlansCurrency}}
|
||||
/>
|
||||
{{svg-jar "arrow-down-small"}}
|
||||
</span>
|
||||
</GhFormGroup>
|
||||
</div>
|
||||
<div class="w-100 flex flex-column flex-row-ns">
|
||||
<div class="w-100 w-50-ns mr3-ns">
|
||||
<GhFormGroup>
|
||||
<div class="gh-setting-title">Monthly price</div>
|
||||
|
||||
<div class="flex items-center justify-center mt2 gh-input-group gh-labs-price-label">
|
||||
<GhTextInput
|
||||
@disabled={{this.disabled}}
|
||||
@value={{readonly this.stripeMonthlyAmount}}
|
||||
@type="number"
|
||||
@input={{action (mut this.stripeMonthlyAmount) value="target.value"}}
|
||||
{{on "blur" this.validateStripePlans}}
|
||||
/>
|
||||
<span class="gh-input-append"><span class="ttu">{{this.currency}}</span>/month</span>
|
||||
</div>
|
||||
</GhFormGroup>
|
||||
</div>
|
||||
<div class="w-100 w-50-ns ml2-ns">
|
||||
<GhFormGroup>
|
||||
<div class="gh-setting-title">Yearly price</div>
|
||||
<div class="flex items-center justify-center mt2 gh-input-group gh-labs-price-label">
|
||||
<GhTextInput
|
||||
@disabled={{this.disabled}}
|
||||
@value={{readonly this.stripeYearlyAmount}}
|
||||
@type="number"
|
||||
@input={{action (mut this.stripeYearlyAmount) value="target.value"}}
|
||||
{{on "blur" this.validateStripePlans}}
|
||||
/>
|
||||
<span class="gh-input-append"><span class="ttu">{{this.currency}}</span>/year</span>
|
||||
</div>
|
||||
</GhFormGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 w-50-l flex flex-column flex-row-ns">
|
||||
{{#if this.stripePlanError}}
|
||||
<p class="response w-100 red"> {{this.stripePlanError}} </p>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="gh-stack-item gh-setting flex-column">
|
||||
<div class="gh-setting-title">Plans available at signup</div>
|
||||
<div class="form-group mt2 mb0 for-checkbox">
|
||||
<label
|
||||
class="checkbox"
|
||||
for="free-plan"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={{this.isFreeChecked}}
|
||||
id="free-plan"
|
||||
name="free-plan"
|
||||
disabled={{this.isFreeDisabled}}
|
||||
class="gh-input post-settings-featured"
|
||||
{{on "click" this.toggleFreePlan}}
|
||||
data-test-checkbox="featured"
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
<p>Free</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group mb0 for-checkbox">
|
||||
<label
|
||||
class="checkbox"
|
||||
for="monthly-plan"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="monthly-plan"
|
||||
name="monthly-plan"
|
||||
checked={{this.isMonthlyChecked}}
|
||||
disabled={{not this.membersUtils.isStripeEnabled}}
|
||||
class="gh-input post-settings-featured"
|
||||
{{on "click" this.toggleMonthlyPlan}}
|
||||
data-test-checkbox="featured"
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
<p>Monthly</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group mb0 for-checkbox">
|
||||
<label
|
||||
class="checkbox"
|
||||
for="yearly-plan"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="yearly-plan"
|
||||
name="yearly-plan"
|
||||
checked={{this.isYearlyChecked}}
|
||||
disabled={{not this.membersUtils.isStripeEnabled}}
|
||||
class="gh-input post-settings-featured"
|
||||
{{on "click" this.toggleYearlyPlan}}
|
||||
data-test-checkbox="featured"
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
<p>Yearly</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="gh-launch-wizard-nav-buttons">
|
||||
<button type="button" class="gh-btn gh-btn-outline gh-btn-icon-dark gh-btn-large w-30" {{on "click" this.backStep}}><span>{{svg-jar "arrow-left-tail"}}</span></button>
|
||||
|
||||
{{!-- TODO: reset "failed" state automatically --}}
|
||||
<GhTaskButton
|
||||
@task={{this.saveAndContinue}}
|
||||
@runningText="Saving"
|
||||
@class="w-70 ml4 right gh-btn gh-btn-black gh-btn-large gh-btn-icon-right"
|
||||
data-test-button="wizard-next"
|
||||
>
|
||||
<span>{{if this.isHidden "Continue" "Save and continue"}}{{svg-jar "arrow-right-tail"}}</span>
|
||||
</GhTaskButton>
|
||||
</div>
|
||||
</div>
|
@ -1,209 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import envConfig from 'ghost-admin/config/environment';
|
||||
import {action} from '@ember/object';
|
||||
import {currencies, getCurrencyOptions, getSymbol} from 'ghost-admin/utils/currency';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
const CURRENCIES = currencies.map((currency) => {
|
||||
return {
|
||||
value: currency.isoCode.toLowerCase(),
|
||||
label: `${currency.isoCode} - ${currency.name}`,
|
||||
isoCode: currency.isoCode
|
||||
};
|
||||
});
|
||||
|
||||
export default class GhLaunchWizardSetPricingComponent extends Component {
|
||||
@service config;
|
||||
@service membersUtils;
|
||||
@service settings;
|
||||
@service store;
|
||||
|
||||
currencies = CURRENCIES;
|
||||
|
||||
@tracked stripeMonthlyAmount = 5;
|
||||
@tracked stripeYearlyAmount = 50;
|
||||
@tracked currency = 'usd';
|
||||
@tracked isFreeChecked = true;
|
||||
@tracked isMonthlyChecked = true;
|
||||
@tracked isYearlyChecked = true;
|
||||
@tracked stripePlanError = '';
|
||||
@tracked tier;
|
||||
@tracked loadingTier = false;
|
||||
|
||||
get selectedCurrency() {
|
||||
return this.currencies.findBy('value', this.currency);
|
||||
}
|
||||
|
||||
get allCurrencies() {
|
||||
return getCurrencyOptions();
|
||||
}
|
||||
|
||||
get isConnectDisallowed() {
|
||||
const siteUrl = this.config.get('blogUrl');
|
||||
|
||||
return envConfig.environment !== 'development' && !/^https:/.test(siteUrl);
|
||||
}
|
||||
|
||||
get isFreeDisabled() {
|
||||
return this.settings.get('membersSignupAccess') !== 'all';
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy?.(...arguments);
|
||||
// clear any unsaved settings changes when going back/forward/closing
|
||||
this.args.updatePreview('');
|
||||
}
|
||||
|
||||
@action
|
||||
setup() {
|
||||
this.fetchDefaultTier.perform();
|
||||
this.updatePreviewUrl();
|
||||
}
|
||||
|
||||
@action
|
||||
backStep() {
|
||||
const tier = this.tier;
|
||||
const data = this.args.getData() || {};
|
||||
this.args.storeData({
|
||||
...data,
|
||||
tier,
|
||||
isFreeChecked: this.isFreeChecked,
|
||||
isMonthlyChecked: this.isMonthlyChecked,
|
||||
isYearlyChecked: this.isYearlyChecked,
|
||||
monthlyAmount: this.stripeMonthlyAmount,
|
||||
yearlyAmount: this.stripeYearlyAmount,
|
||||
currency: this.currency
|
||||
});
|
||||
this.args.backStep();
|
||||
}
|
||||
|
||||
@action
|
||||
setStripePlansCurrency(event) {
|
||||
const newCurrency = event.value;
|
||||
this.currency = newCurrency;
|
||||
this.updatePreviewUrl();
|
||||
}
|
||||
|
||||
@action
|
||||
toggleFreePlan(event) {
|
||||
this.isFreeChecked = event.target.checked;
|
||||
this.updatePreviewUrl();
|
||||
}
|
||||
|
||||
@action
|
||||
toggleMonthlyPlan(event) {
|
||||
this.isMonthlyChecked = event.target.checked;
|
||||
this.updatePreviewUrl();
|
||||
}
|
||||
|
||||
@action
|
||||
toggleYearlyPlan(event) {
|
||||
this.isYearlyChecked = event.target.checked;
|
||||
this.updatePreviewUrl();
|
||||
}
|
||||
|
||||
@action
|
||||
validateStripePlans() {
|
||||
this.stripePlanError = undefined;
|
||||
|
||||
try {
|
||||
const yearlyAmount = this.stripeYearlyAmount;
|
||||
const monthlyAmount = this.stripeMonthlyAmount;
|
||||
const symbol = getSymbol(this.currency);
|
||||
if (!yearlyAmount || yearlyAmount < 1 || !monthlyAmount || monthlyAmount < 1) {
|
||||
throw new TypeError(`Subscription amount must be at least ${symbol}1.00`);
|
||||
}
|
||||
|
||||
this.updatePreviewUrl();
|
||||
} catch (err) {
|
||||
this.stripePlanError = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
*saveAndContinue() {
|
||||
if (this.isConnectDisallowed) {
|
||||
this.args.nextStep();
|
||||
} else {
|
||||
yield this.validateStripePlans();
|
||||
|
||||
if (this.stripePlanError) {
|
||||
return false;
|
||||
}
|
||||
const tier = this.tier;
|
||||
const data = this.args.getData() || {};
|
||||
this.args.storeData({
|
||||
...data,
|
||||
tier,
|
||||
isFreeChecked: this.isFreeChecked,
|
||||
isMonthlyChecked: this.isMonthlyChecked,
|
||||
isYearlyChecked: this.isYearlyChecked,
|
||||
monthlyAmount: this.stripeMonthlyAmount,
|
||||
yearlyAmount: this.stripeYearlyAmount,
|
||||
currency: this.currency
|
||||
});
|
||||
this.args.nextStep();
|
||||
}
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*fetchDefaultTier() {
|
||||
const storedData = this.args.getData();
|
||||
if (storedData?.tier) {
|
||||
this.tier = storedData.tier;
|
||||
|
||||
if (storedData.isMonthlyChecked !== undefined) {
|
||||
this.isMonthlyChecked = storedData.isMonthlyChecked;
|
||||
}
|
||||
if (storedData.isYearlyChecked !== undefined) {
|
||||
this.isYearlyChecked = storedData.isYearlyChecked;
|
||||
}
|
||||
if (storedData.isFreeChecked !== undefined) {
|
||||
this.isFreeChecked = storedData.isFreeChecked;
|
||||
}
|
||||
if (storedData.currency !== undefined) {
|
||||
this.currency = storedData.currency;
|
||||
}
|
||||
this.stripeMonthlyAmount = storedData.monthlyAmount;
|
||||
this.stripeYearlyAmount = storedData.yearlyAmount;
|
||||
} else {
|
||||
const tiers = yield this.store.query('tier', {filter: 'type:paid', include: 'monthly_price,yearly_price'});
|
||||
this.tier = tiers.firstObject;
|
||||
|
||||
let portalPlans = this.settings.get('portalPlans') || [];
|
||||
|
||||
this.isMonthlyChecked = portalPlans.includes('monthly');
|
||||
this.isYearlyChecked = portalPlans.includes('yearly');
|
||||
this.isFreeChecked = portalPlans.includes('free');
|
||||
|
||||
const monthlyPrice = this.tier.get('monthlyPrice');
|
||||
const yearlyPrice = this.tier.get('yearlyPrice');
|
||||
if (monthlyPrice && monthlyPrice.amount) {
|
||||
this.stripeMonthlyAmount = (monthlyPrice.amount / 100);
|
||||
this.currency = monthlyPrice.currency;
|
||||
}
|
||||
if (yearlyPrice && yearlyPrice.amount) {
|
||||
this.stripeYearlyAmount = (yearlyPrice.amount / 100);
|
||||
}
|
||||
}
|
||||
this.updatePreviewUrl();
|
||||
}
|
||||
|
||||
updatePreviewUrl() {
|
||||
const options = {
|
||||
disableBackground: true,
|
||||
currency: this.selectedCurrency.value,
|
||||
monthlyPrice: Math.round(this.stripeMonthlyAmount * 100),
|
||||
yearlyPrice: Math.round(this.stripeYearlyAmount * 100),
|
||||
isMonthlyChecked: this.isMonthlyChecked,
|
||||
isYearlyChecked: this.isYearlyChecked,
|
||||
isFreeChecked: this.isFreeChecked,
|
||||
portalPlans: null
|
||||
};
|
||||
|
||||
const url = this.membersUtils.getPortalPreviewUrl(options);
|
||||
this.args.updatePreview(url);
|
||||
}
|
||||
}
|
@ -1,163 +0,0 @@
|
||||
import Controller from '@ember/controller';
|
||||
import envConfig from 'ghost-admin/config/environment';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
const DEFAULT_STEPS = {
|
||||
'customise-design': {
|
||||
title: 'Customise your site',
|
||||
position: 'Step 1',
|
||||
next: 'connect-stripe'
|
||||
},
|
||||
'connect-stripe': {
|
||||
title: 'Connect to Stripe',
|
||||
position: 'Step 2',
|
||||
next: 'set-pricing',
|
||||
back: 'customise-design',
|
||||
skip: 'finalise'
|
||||
},
|
||||
'set-pricing': {
|
||||
title: 'Set up subscriptions',
|
||||
position: 'Step 3',
|
||||
next: 'finalise',
|
||||
back: 'connect-stripe'
|
||||
},
|
||||
finalise: {
|
||||
title: 'Launch your site',
|
||||
position: 'Final step',
|
||||
back: 'set-pricing'
|
||||
}
|
||||
};
|
||||
export default class LaunchController extends Controller {
|
||||
@service config;
|
||||
@service router;
|
||||
@service settings;
|
||||
|
||||
queryParams = ['step'];
|
||||
|
||||
@tracked previewGuid = (new Date()).valueOf();
|
||||
@tracked previewSrc = '';
|
||||
@tracked step = 'customise-design';
|
||||
@tracked data = null;
|
||||
|
||||
steps = DEFAULT_STEPS;
|
||||
|
||||
skippedSteps = [];
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
const siteUrl = this.config.get('blogUrl');
|
||||
|
||||
if (envConfig.environment !== 'development' && !/^https:/.test(siteUrl)) {
|
||||
this.steps = {
|
||||
'customise-design': {
|
||||
title: 'Customise your site',
|
||||
position: 'Step 1',
|
||||
next: 'set-pricing'
|
||||
},
|
||||
'set-pricing': {
|
||||
title: 'Set up subscriptions',
|
||||
position: 'Step 2',
|
||||
next: 'finalise',
|
||||
back: 'customise-design'
|
||||
},
|
||||
finalise: {
|
||||
title: 'Launch your site',
|
||||
position: 'Final step',
|
||||
back: 'set-pricing'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
this.steps = DEFAULT_STEPS;
|
||||
}
|
||||
}
|
||||
|
||||
get currentStep() {
|
||||
return this.steps[this.step];
|
||||
}
|
||||
|
||||
@action
|
||||
storeData(data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@action
|
||||
getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
@action
|
||||
goToStep(step) {
|
||||
if (step) {
|
||||
this.step = step;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
goNextStep() {
|
||||
this.step = this.currentStep.next;
|
||||
}
|
||||
|
||||
@action
|
||||
goBackStep() {
|
||||
let step = this.currentStep.back;
|
||||
|
||||
while (this.skippedSteps.includes(step)) {
|
||||
this.skippedSteps = this.skippedSteps.filter(s => s !== step);
|
||||
step = this.steps[step].back;
|
||||
}
|
||||
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
// TODO: remember when a step is skipped so "back" works as expected
|
||||
@action
|
||||
skipStep() {
|
||||
let step = this.currentStep.next;
|
||||
let skipToStep = this.currentStep.skip;
|
||||
|
||||
while (step !== skipToStep) {
|
||||
this.skippedSteps.push(step);
|
||||
step = this.steps[step].next;
|
||||
}
|
||||
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
@action
|
||||
registerPreviewIframe(element) {
|
||||
this.previewIframe = element;
|
||||
}
|
||||
|
||||
@action
|
||||
refreshPreview() {
|
||||
this.previewGuid = (new Date()).valueOf();
|
||||
}
|
||||
|
||||
@action
|
||||
updatePreview(url) {
|
||||
this.previewSrc = url;
|
||||
}
|
||||
|
||||
@action
|
||||
replacePreviewContents(html) {
|
||||
if (this.previewIframe) {
|
||||
this.previewIframe.contentWindow.document.open();
|
||||
this.previewIframe.contentWindow.document.write(html);
|
||||
this.previewIframe.contentWindow.document.close();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
close() {
|
||||
this.router.transitionTo('dashboard');
|
||||
}
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.data = null;
|
||||
this.step = 'customise-design';
|
||||
this.skippedSteps = [];
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default class LaunchRoute extends AuthenticatedRoute {
|
||||
@service session;
|
||||
|
||||
beforeModel() {
|
||||
super.beforeModel(...arguments);
|
||||
if (!this.session.user.isOwnerOnly) {
|
||||
return this.transitionTo('home');
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
<div class="fullscreen-wizard-container" {{will-destroy this.reset}}>
|
||||
<div class="pt7 pb5 pl12 pr12 flex justify-between items-center">
|
||||
<div class="flex flex-column">
|
||||
<div class="ttu gh-launch-wizard-step-indicator">{{this.currentStep.position}}</div>
|
||||
<h2>{{this.currentStep.title}}</h2>
|
||||
</div>
|
||||
<button type="button" class="close gh-btn gh-btn-outline" {{on "click" this.close}} data-test-button="close-wizard">
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="gh-launch-wizard-content">
|
||||
<div class="gh-launch-wizard-content-left">
|
||||
{{component (concat "gh-launch-wizard/" this.step)
|
||||
nextStep=this.goNextStep
|
||||
backStep=this.goBackStep
|
||||
skipStep=this.skipStep
|
||||
refreshPreview=this.refreshPreview
|
||||
updatePreview=this.updatePreview
|
||||
replacePreviewContents=this.replacePreviewContents
|
||||
storeData=this.storeData
|
||||
getData=this.getData
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div class="gh-launch-wizard-content-right">
|
||||
<GhBrowserPreview class="gh-launch-wizard-preview-container" @icon={{this.settings.icon}} @title={{this.config.blogTitle}}>
|
||||
<GhSiteIframe class="gh-launch-wizard-preview" @src={{this.previewSrc}} @guid={{this.previewGuid}} {{did-insert this.registerPreviewIframe}}></GhSiteIframe>
|
||||
</GhBrowserPreview>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,30 +0,0 @@
|
||||
import {authenticateSession} from 'ember-simple-auth/test-support';
|
||||
import {currentURL, visit} from '@ember/test-helpers';
|
||||
import {describe, it} from 'mocha';
|
||||
import {expect} from 'chai';
|
||||
import {setupApplicationTest} from 'ember-mocha';
|
||||
import {setupMirage} from 'ember-cli-mirage/test-support';
|
||||
|
||||
describe('Acceptance: Launch flow', function () {
|
||||
const hooks = setupApplicationTest();
|
||||
setupMirage(hooks);
|
||||
|
||||
it('is not accessible when logged out', async function () {
|
||||
await visit('/launch');
|
||||
expect(currentURL()).to.equal('/signin');
|
||||
});
|
||||
|
||||
describe('when logged in', function () {
|
||||
beforeEach(async function () {
|
||||
let role = this.server.create('role', {name: 'Owner'});
|
||||
this.server.create('user', {roles: [role]});
|
||||
|
||||
return await authenticateSession();
|
||||
});
|
||||
|
||||
it('can visit /launch', async function () {
|
||||
await visit('/launch');
|
||||
expect(currentURL()).to.equal('/launch');
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user