зеркало из https://github.com/mozilla/fxa.git
Родитель
26259c6659
Коммит
c7ec3143df
|
@ -45,7 +45,10 @@ module.exports = (log, db, config, customs, zendeskClient) => {
|
|||
validate: {
|
||||
payload: isA.object().keys({
|
||||
topic: isA.string().required(),
|
||||
subject: isA.string().optional(),
|
||||
subject: isA
|
||||
.string()
|
||||
.allow('')
|
||||
.optional(),
|
||||
message: isA.string().required(),
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -5,13 +5,27 @@
|
|||
'use strict';
|
||||
|
||||
const PaymentServer = {
|
||||
navigateToPaymentServer(view, subscriptionsConfig, redirectPath) {
|
||||
navigateToPaymentServer(
|
||||
view,
|
||||
subscriptionsConfig,
|
||||
redirectPath,
|
||||
queryParams
|
||||
) {
|
||||
const {
|
||||
managementClientId,
|
||||
managementScopes,
|
||||
managementTokenTTL,
|
||||
managementUrl,
|
||||
} = subscriptionsConfig;
|
||||
const queryString =
|
||||
typeof queryParams === 'object' &&
|
||||
Object.keys(queryParams)
|
||||
.filter(k => queryParams[k] !== null && queryParams[k] !== '')
|
||||
.map(
|
||||
k => `${encodeURIComponent(k)}=${encodeURIComponent(queryParams[k])}`
|
||||
)
|
||||
.join('&');
|
||||
const qs = queryString.length ? `?${queryString}` : '';
|
||||
return view
|
||||
.getSignedInAccount()
|
||||
.createOAuthToken(managementClientId, {
|
||||
|
@ -19,7 +33,7 @@ const PaymentServer = {
|
|||
ttl: managementTokenTTL,
|
||||
})
|
||||
.then(accessToken => {
|
||||
const url = `${managementUrl}/${redirectPath}#accessToken=${encodeURIComponent(
|
||||
const url = `${managementUrl}/${redirectPath}${qs}#accessToken=${encodeURIComponent(
|
||||
accessToken.get('token')
|
||||
)}`;
|
||||
view.navigateAway(url);
|
||||
|
|
|
@ -32,6 +32,7 @@ import ForceAuthView from '../views/force_auth';
|
|||
import IndexView from '../views/index';
|
||||
import OAuthIndexView from '../views/oauth_index';
|
||||
import PermissionsView from '../views/permissions';
|
||||
import SupportView from '../views/support';
|
||||
import ReadyView from '../views/ready';
|
||||
import RecoveryCodesView from '../views/settings/recovery_codes';
|
||||
import RedirectAuthView from '../views/authorization';
|
||||
|
@ -261,9 +262,8 @@ const Router = Backbone.Router.extend({
|
|||
'subscriptions/products/:productId': createViewHandler(
|
||||
SubscriptionsProductRedirectView
|
||||
),
|
||||
'subscriptions(/)': createViewHandler(
|
||||
SubscriptionsManagementRedirectView
|
||||
),
|
||||
'subscriptions(/)': createViewHandler(SubscriptionsManagementRedirectView),
|
||||
'support(/)': createViewHandler(SupportView),
|
||||
'verify_email(/)': createViewHandler(CompleteSignUpView, {
|
||||
type: VerificationReasons.SIGN_UP,
|
||||
}),
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<div class="modal dialog-error">
|
||||
<div class="message">
|
||||
<p>{{#t}}Hmm, we're having trouble with our system. We're working on fixing it for you and apologize for the inconvenience. Please try again later.{{/t}}</p>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,78 @@
|
|||
<div id="fxa-settings-header-wrapper">
|
||||
<header id="fxa-settings-header">
|
||||
<h1 id="fxa-manage-account"><span class="fxa-account-title">{{#t}}Firefox Accounts{{/t}}</span></h1>
|
||||
</header>
|
||||
|
||||
<div class="settings-success-wrapper">
|
||||
<div class="success settings-success"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fxa-settings">
|
||||
<div id="fxa-settings-content" class="card">
|
||||
|
||||
<header id="fxa-settings-profile-header-wrapper">
|
||||
<div class="avatar-wrapper avatar-settings-view nohover"></div>
|
||||
<div id="fxa-settings-profile-header">
|
||||
{{{ unsafeHeaderHTML }}}
|
||||
</div>
|
||||
</header>
|
||||
<div class="child-views">
|
||||
<div class="settings-child-view support">
|
||||
<div class="settings-unit">
|
||||
<div class="settings-unit-stub">
|
||||
<header class="settings-unit-summary">
|
||||
<h2 class="settings-unit-title">Subscriptions</h2>
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-unit">
|
||||
<div class="support-form">
|
||||
<header>
|
||||
<h3>{{#t}}Contact Us{{/t}}</h3>
|
||||
</header>
|
||||
<form id="fxa-support" novalidate>
|
||||
<div>
|
||||
<div class="form-label">
|
||||
<label for="topic">{{#t}}What do you need help with?{{/t}}</label>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<select id="topic" name="topic" data-placeholder="{{topicPlaceHolder}}">
|
||||
<option value=""></option>
|
||||
<option value="General inquiries">{{#t}}General inquiries{{/t}}</option>
|
||||
<option value="Payment & billing">{{#t}}Payment & billing{{/t}}</option>
|
||||
<option value="Connection issues">{{#t}}Connection issues{{/t}}</option>
|
||||
<option value="Getting started">{{#t}}Getting started{{/t}}</option>
|
||||
<option value="Account issues">{{#t}}Account issues{{/t}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-label">
|
||||
<label for="subject">{{#t}}Subject (optional){{/t}}</label>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<input type="text" id="subject" name="subject"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-label">
|
||||
<label for="message">{{#t}}Description of Issue{{/t}}</label>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<textarea id="message" name="message" rows="5" cols="70"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button class="settings-button secondary-button cancel">{{#t}}Cancel{{/t}}</button>
|
||||
<button type="submit" class="settings-button primary-button" disabled>{{#t}}Send{{/t}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer id="legal-footer"><a class="terms" href="/legal/terms">{{#t}}Terms of Service{{/t}}</a><a class="privacy" href="/legal/privacy">{{#t}}Privacy Notice{{/t}}</a></footer>
|
|
@ -0,0 +1,34 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Session from '../../lib/session';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Set the user's signed in account with the relier's uid.
|
||||
* Used in settings and support views.
|
||||
*/
|
||||
getUidAndSetSignedInAccount() {
|
||||
const uid = this.relier.get('uid');
|
||||
this.notifier.trigger('set-uid', uid);
|
||||
|
||||
// A uid param is set by RPs linking directly to the settings
|
||||
// page for a particular account.
|
||||
//
|
||||
// We set the current account to the one with `uid` if
|
||||
// it exists in our list of cached accounts. If the account is
|
||||
// not in the list of cached accounts, clear the current account.
|
||||
//
|
||||
if (!this.user.getAccountByUid(uid).isDefault()) {
|
||||
// The account with uid exists; set it to our current account.
|
||||
this.user.setSignedInAccountByUid(uid);
|
||||
} else if (uid) {
|
||||
// session is expired or user does not exist. Force the user
|
||||
// to sign in.
|
||||
Session.clear();
|
||||
this.user.clearSignedInAccount();
|
||||
this.logViewEvent('signout.forced');
|
||||
}
|
||||
},
|
||||
};
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import $ from 'jquery';
|
||||
import allowOnlyOneSubmit from './decorators/allow_only_one_submit';
|
||||
import AccountByUidMixin from './mixins/account-by-uid-mixin';
|
||||
import AccountRecoveryView from './settings/account_recovery/account_recovery';
|
||||
import AccountRecoveryConfirmPasswordView from './settings/account_recovery/confirm_password';
|
||||
import AccountRecoveryConfirmRevokeView from './settings/account_recovery/confirm_revoke';
|
||||
|
@ -27,7 +28,6 @@ import EmailsView from './settings/emails';
|
|||
import LoadingMixin from './mixins/loading-mixin';
|
||||
import 'modal';
|
||||
import preventDefaultThen from './decorators/prevent_default_then';
|
||||
import Session from '../lib/session';
|
||||
import SettingsHeaderTemplate from 'templates/partial/settings-header.mustache';
|
||||
import SignedOutNotificationMixin from './mixins/signed-out-notification-mixin';
|
||||
import SubPanels from './sub_panels';
|
||||
|
@ -77,27 +77,7 @@ const View = BaseView.extend({
|
|||
this._subscriptionsManagementEnabled =
|
||||
options.subscriptionsManagementEnabled !== false;
|
||||
|
||||
const uid = this.relier.get('uid');
|
||||
this.notifier.trigger('set-uid', uid);
|
||||
|
||||
// A uid param is set by RPs linking directly to the settings
|
||||
// page for a particular account.
|
||||
//
|
||||
// We set the current account to the one with `uid` if
|
||||
// it exists in our list of cached accounts. If the account is
|
||||
// not in the list of cached accounts, clear the current account.
|
||||
//
|
||||
// The `mustVerify` flag will ensure that the account is valid.
|
||||
if (!this.user.getAccountByUid(uid).isDefault()) {
|
||||
// The account with uid exists; set it to our current account.
|
||||
this.user.setSignedInAccountByUid(uid);
|
||||
} else if (uid) {
|
||||
// session is expired or user does not exist. Force the user
|
||||
// to sign in.
|
||||
Session.clear();
|
||||
this.user.clearSignedInAccount();
|
||||
this.logViewEvent('signout.forced');
|
||||
}
|
||||
this.getUidAndSetSignedInAccount();
|
||||
},
|
||||
|
||||
notifications: {
|
||||
|
@ -316,6 +296,7 @@ const View = BaseView.extend({
|
|||
|
||||
Cocktail.mixin(
|
||||
View,
|
||||
AccountByUidMixin,
|
||||
AvatarMixin,
|
||||
LoadingMixin,
|
||||
SignedOutNotificationMixin,
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import $ from 'jquery';
|
||||
import allowOnlyOneSubmit from './decorators/allow_only_one_submit';
|
||||
import AccountByUidMixin from './mixins/account-by-uid-mixin';
|
||||
import AvatarMixin from './mixins/avatar-mixin';
|
||||
import BaseView from './base';
|
||||
import 'chosen-js';
|
||||
import Cocktail from 'cocktail';
|
||||
import LoadingMixin from './mixins/loading-mixin';
|
||||
import 'modal';
|
||||
import PaymentServer from '../lib/payment-server';
|
||||
import preventDefaultThen from './decorators/prevent_default_then';
|
||||
import SettingsHeaderTemplate from 'templates/partial/settings-header.mustache';
|
||||
import SupportFormErrorTemplate from 'templates/partial/support-form-error.mustache';
|
||||
import Template from 'templates/support.mustache';
|
||||
|
||||
const t = msg => msg;
|
||||
|
||||
const proto = BaseView.prototype;
|
||||
const SupportView = BaseView.extend({
|
||||
template: Template,
|
||||
className: 'settings',
|
||||
layoutClassName: 'settings',
|
||||
viewName: 'support',
|
||||
|
||||
// The `mustVerify` flag will ensure that the account is valid.
|
||||
mustVerify: true,
|
||||
|
||||
initialize(options = {}) {
|
||||
this.getUidAndSetSignedInAccount();
|
||||
|
||||
this._subscriptionsConfig = {};
|
||||
if (options && options.config && options.config.subscriptions) {
|
||||
this._subscriptionsConfig = options.config.subscriptions;
|
||||
}
|
||||
},
|
||||
|
||||
setInitialContext(context) {
|
||||
const account = this.getSignedInAccount();
|
||||
context.set({
|
||||
unsafeHeaderHTML: this._getHeaderHTML(account),
|
||||
topicPlaceHolder: t('Please Select One'),
|
||||
});
|
||||
},
|
||||
|
||||
events: {
|
||||
'change #topic': 'onFormChange',
|
||||
'keyup #message': 'onFormChange',
|
||||
'click button[type=submit]': 'submitSupportForm',
|
||||
'click button.cancel': 'navigateToSubscriptionsManagement',
|
||||
},
|
||||
|
||||
beforeRender() {
|
||||
const account = this.getSignedInAccount();
|
||||
|
||||
return account.fetchActiveSubscriptions().then(subscriptions => {
|
||||
if (subscriptions.length > 0) {
|
||||
return account.fetchProfile().then(() => this.user.setAccount(account));
|
||||
} else {
|
||||
// Note that if a user landed here, it is because:
|
||||
// a) they accessed the page directly, as the button for this page is
|
||||
// not displayed when a user does not have any active subscriptions
|
||||
// b) the edge case where the (last) subscription expired between
|
||||
// clicking of the button and here
|
||||
|
||||
this.navigateToSubscriptionsManagement();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_getHeaderHTML(account) {
|
||||
return SettingsHeaderTemplate(account.pick('displayName', 'email'));
|
||||
},
|
||||
|
||||
afterVisible() {
|
||||
this.topicEl = this.$('#topic');
|
||||
this.submitBtn = this.$('button[type="submit"]');
|
||||
this.subjectEl = this.$('#subject');
|
||||
this.messageEl = this.$('#message');
|
||||
this.topicEl.chosen({ disable_search: true, width: '100%' });
|
||||
|
||||
return proto.afterVisible.call(this).then(this._showAvatar.bind(this));
|
||||
},
|
||||
|
||||
_showAvatar() {
|
||||
var account = this.getSignedInAccount();
|
||||
return this.displayAccountProfileImage(account);
|
||||
},
|
||||
|
||||
onFormChange(e) {
|
||||
e.stopPropagation();
|
||||
|
||||
if (this.messageEl.val().trim() !== '' && this.topicEl.val() !== '') {
|
||||
this.submitBtn.attr('disabled', false);
|
||||
} else {
|
||||
this.submitBtn.attr('disabled', true);
|
||||
}
|
||||
},
|
||||
|
||||
submitSupportForm: preventDefaultThen(
|
||||
allowOnlyOneSubmit(function() {
|
||||
const account = this.getSignedInAccount();
|
||||
const supportTicket = this.buildSupportTicket();
|
||||
return account
|
||||
.createSupportTicket(supportTicket)
|
||||
.then(this.handleFormResponse.bind(this))
|
||||
.catch(this.displayErrorMessage.bind(this));
|
||||
})
|
||||
),
|
||||
|
||||
handleFormResponse(resp) {
|
||||
if (resp.success === true) {
|
||||
this.navigateToSubscriptionsManagement({
|
||||
successfulSupportTicketSubmission: true,
|
||||
});
|
||||
} else {
|
||||
this.displayErrorMessage();
|
||||
}
|
||||
},
|
||||
|
||||
displayErrorMessage() {
|
||||
// Inject the error modal if it's not already there.
|
||||
if (!$('.modal').length) {
|
||||
const errorModal = this.renderTemplate(SupportFormErrorTemplate);
|
||||
$('body').append(errorModal);
|
||||
}
|
||||
$('.modal').modal({
|
||||
closeText: '✕',
|
||||
closeClass: 'icon-remove',
|
||||
});
|
||||
},
|
||||
|
||||
navigateToSubscriptionsManagement(queryParams = {}) {
|
||||
PaymentServer.navigateToPaymentServer(
|
||||
this,
|
||||
this._subscriptionsConfig,
|
||||
'subscriptions',
|
||||
queryParams
|
||||
);
|
||||
},
|
||||
|
||||
buildSupportTicket() {
|
||||
return {
|
||||
topic: this.topicEl.val(),
|
||||
subject: this.subjectEl.val().trim(),
|
||||
message: this.messageEl.val().trim(),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Cocktail.mixin(SupportView, AccountByUidMixin, AvatarMixin, LoadingMixin);
|
||||
|
||||
export default SupportView;
|
|
@ -17,3 +17,5 @@
|
|||
@import 'modules/graphic';
|
||||
@import 'modules/marketing';
|
||||
@import 'modules/marketing-ios';
|
||||
@import 'modules/support';
|
||||
@import 'modules/chosen';
|
||||
|
|
|
@ -48,11 +48,11 @@ button {
|
|||
background: $button-background-color-primary;
|
||||
color: $button-text-color-primary;
|
||||
|
||||
&:hover:not:disabled {
|
||||
&:hover:enabled {
|
||||
background: $button-background-color-primary-hover;
|
||||
}
|
||||
|
||||
&:active {
|
||||
&:active:enabled {
|
||||
background: $button-background-color-primary-active;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Styles for Chosen: https://github.com/harvesthq/chosen
|
||||
*/
|
||||
|
||||
.chosen-container {
|
||||
user-select: none;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chosen-drop {
|
||||
background: #fff;
|
||||
border: 1px solid #aaa;
|
||||
border-top: 0;
|
||||
box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15);
|
||||
clip: rect(0, 0, 0, 0);
|
||||
clip-path: inset(100% 100%);
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&.chosen-with-drop {
|
||||
.chosen-drop {
|
||||
clip: auto;
|
||||
clip-path: none;
|
||||
}
|
||||
|
||||
.chosen-single {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chosen-container-single {
|
||||
.chosen-single {
|
||||
border: 1px solid #aaa;
|
||||
border-radius: $input-border-radius;
|
||||
color: #20123b;
|
||||
display: block;
|
||||
font-size: $input-text-font-size-default;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
|
||||
@include respond-to('big') {
|
||||
height: $input-height;
|
||||
line-height: $input-height;
|
||||
}
|
||||
|
||||
@include respond-to('small') {
|
||||
height: $input-height-small;
|
||||
line-height: $input-height-small;
|
||||
}
|
||||
|
||||
html[dir='ltr'] & {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
html[dir='rtl'] & {
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.chosen-default {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.chosen-single {
|
||||
div {
|
||||
border-color: #686869 transparent transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 7px 8px 0 8px;
|
||||
display: block;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
|
||||
@include respond-to('big') {
|
||||
top: 20px;
|
||||
|
||||
html[dir='ltr'] & {
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
html[dir='rtl'] & {
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@include respond-to('small') {
|
||||
top: 16px;
|
||||
|
||||
html[dir='ltr'] & {
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
html[dir='rtl'] & {
|
||||
left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@at-root .chosen-with-drop#{&} {
|
||||
border-color: transparent transparent #686869 transparent;
|
||||
border-width: 0 8px 7px 8px;
|
||||
}
|
||||
|
||||
b {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chosen-drop {
|
||||
background-clip: padding-box;
|
||||
border-radius: 0 0 $input-border-radius $input-border-radius;
|
||||
}
|
||||
|
||||
&.chosen-container-single-nosearch .chosen-search {
|
||||
clip-path: inset(100% 100%);
|
||||
clip: rect(0, 0, 0, 0);
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.chosen-container {
|
||||
.chosen-results {
|
||||
color: #000;
|
||||
font-size: $input-text-font-size-default;
|
||||
margin: 8px 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
|
||||
li {
|
||||
display: none;
|
||||
line-height: $input-height;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding-left: $input-left-right-padding;
|
||||
padding-right: $input-left-right-padding;
|
||||
|
||||
&.active-result {
|
||||
cursor: pointer;
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
background-color: #d8d8d8;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chosen-container-active {
|
||||
.chosen-single {
|
||||
border: 1px solid #5897fb;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
&.chosen-with-drop .chosen-single {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border: 1px solid #aaa;
|
||||
box-shadow: 0 1px 0 #fff inset;
|
||||
}
|
||||
|
||||
.chosen-choices {
|
||||
border: 1px solid #5897fb;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
.support {
|
||||
.settings-unit:first-child {
|
||||
@include respond-to('small') {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include respond-to('big') {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-unit-stub {
|
||||
.settings-unit-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.support-form {
|
||||
margin: 25px 32px;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@include input-element();
|
||||
display: inline-block;
|
||||
height: 101px;
|
||||
padding: $input-left-right-padding;
|
||||
|
||||
html[dir='rtl'] & {
|
||||
direction: ltr;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@include respond-to('small') {
|
||||
height: 101px;
|
||||
}
|
||||
}
|
||||
|
||||
.button-row {
|
||||
.settings-button {
|
||||
height: 40x;
|
||||
width: 128px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-label {
|
||||
margin-bottom: 5px;
|
||||
|
||||
label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal.dialog-error {
|
||||
display: inline-block;
|
||||
padding: 25px 27px;
|
||||
|
||||
.close-modal {
|
||||
background: transparent;
|
||||
color: #0c0c0d;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
right:2px;
|
||||
text-indent: 0;
|
||||
top: 13px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -62,4 +62,20 @@ describe('lib/payment-server-redirect', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects as expected with query string', () => {
|
||||
const REDIRECT_PATH = 'example/path';
|
||||
PaymentServer.navigateToPaymentServer(
|
||||
view,
|
||||
config.subscriptions,
|
||||
REDIRECT_PATH,
|
||||
{ foo: 'bar', fizz: '', quuz: '&buzz', buzz: null }
|
||||
).then(() => {
|
||||
assert.deepEqual(
|
||||
view.navigateAway.args[0][0],
|
||||
`${config.subscriptions.managementUrl}/${REDIRECT_PATH}?foo=bar&quzz=%26buzz#accessToken=MOCK_TOKEN`,
|
||||
'should make the correct call to navigateAway'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import AccountByUidMixin from 'views/mixins/account-by-uid-mixin';
|
||||
import { assert } from 'chai';
|
||||
import BaseView from 'views/base';
|
||||
import Cocktail from 'cocktail';
|
||||
import Notifier from 'lib/channels/notifier';
|
||||
import NullChannel from 'lib/channels/null';
|
||||
import Relier from 'models/reliers/relier';
|
||||
import Session from 'lib/session';
|
||||
import sinon from 'sinon';
|
||||
import TestHelpers from '../../../lib/helpers';
|
||||
import User from 'models/user';
|
||||
|
||||
const UID = TestHelpers.createUid();
|
||||
const View = BaseView.extend({});
|
||||
Cocktail.mixin(View, AccountByUidMixin);
|
||||
|
||||
describe('views/mixins/account-by-uid-mixin', function() {
|
||||
let notifier;
|
||||
let relier;
|
||||
let tabChannelMock;
|
||||
let user;
|
||||
let view;
|
||||
|
||||
beforeEach(function() {
|
||||
relier = new Relier();
|
||||
tabChannelMock = new NullChannel();
|
||||
user = new User();
|
||||
notifier = new Notifier({
|
||||
tabChannel: tabChannelMock,
|
||||
});
|
||||
|
||||
sinon
|
||||
.stub(relier, 'get')
|
||||
.withArgs('uid')
|
||||
.returns(UID);
|
||||
|
||||
view = new View({
|
||||
notifier: notifier,
|
||||
relier: relier,
|
||||
user: user,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
relier.get.restore();
|
||||
|
||||
view.remove();
|
||||
view.destroy();
|
||||
view = null;
|
||||
});
|
||||
|
||||
describe('getUidAndSetSignedInAccount', function() {
|
||||
it('gets the uid from the relier', function() {
|
||||
sinon.spy(notifier, 'trigger');
|
||||
sinon.stub(user, 'clearSignedInAccount');
|
||||
|
||||
view.getUidAndSetSignedInAccount();
|
||||
assert.isTrue(relier.get.calledWith('uid'));
|
||||
assert.isTrue(notifier.trigger.calledWith('set-uid', UID));
|
||||
|
||||
notifier.trigger.restore();
|
||||
user.clearSignedInAccount.restore();
|
||||
});
|
||||
|
||||
it('sets signed in account with uid when account exists', function() {
|
||||
sinon.stub(user, 'getAccountByUid').returns({ isDefault: () => false });
|
||||
sinon.stub(user, 'setSignedInAccountByUid');
|
||||
|
||||
view.getUidAndSetSignedInAccount();
|
||||
assert.isTrue(user.getAccountByUid.calledWith(UID));
|
||||
assert.isTrue(user.setSignedInAccountByUid.calledWith(UID));
|
||||
|
||||
user.setSignedInAccountByUid.restore();
|
||||
user.getAccountByUid.restore();
|
||||
});
|
||||
|
||||
it('forces the user to sign in when account does not exist', function() {
|
||||
sinon.stub(user, 'getAccountByUid').returns({ isDefault: () => true });
|
||||
sinon.stub(user, 'clearSignedInAccount');
|
||||
sinon.stub(Session, 'clear');
|
||||
sinon.stub(view, 'logViewEvent');
|
||||
|
||||
view.getUidAndSetSignedInAccount();
|
||||
assert.isTrue(Session.clear.calledOnce);
|
||||
assert.isTrue(user.clearSignedInAccount.calledOnce);
|
||||
assert.isTrue(view.logViewEvent.calledWith('signout.forced'));
|
||||
|
||||
view.logViewEvent.restore();
|
||||
Session.clear.restore();
|
||||
user.clearSignedInAccount.restore();
|
||||
user.getAccountByUid.restore();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,186 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import $ from 'jquery';
|
||||
import AccountByUidMixin from 'views/mixins/account-by-uid-mixin';
|
||||
import { assert } from 'chai';
|
||||
import Notifier from 'lib/channels/notifier';
|
||||
import ProfileClient from 'lib/profile-client';
|
||||
import Relier from 'models/reliers/relier';
|
||||
import sinon from 'sinon';
|
||||
import TestHelpers from '../../lib/helpers';
|
||||
import User from 'models/user';
|
||||
import SupportView from 'views/support';
|
||||
|
||||
sinon.spy(AccountByUidMixin.getUidAndSetSignedInAccount);
|
||||
|
||||
describe('views/support', function() {
|
||||
let account;
|
||||
let notifier;
|
||||
let profileClient;
|
||||
let relier;
|
||||
let user;
|
||||
let view;
|
||||
|
||||
const email = 'a@a.com';
|
||||
const UID = TestHelpers.createUid();
|
||||
const subscriptionsConfig = { managementClientId: 'OVER9000' };
|
||||
const supportTicket = {
|
||||
topic: 'General inquiries',
|
||||
subject: '',
|
||||
message: 'inquiries from generals',
|
||||
};
|
||||
|
||||
function createSupportView() {
|
||||
view = new SupportView({
|
||||
config: { subscriptions: subscriptionsConfig },
|
||||
notifier,
|
||||
relier,
|
||||
user,
|
||||
});
|
||||
sinon.stub(view, 'checkAuthorization').returns(Promise.resolve(true));
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
notifier = new Notifier();
|
||||
profileClient = new ProfileClient();
|
||||
relier = new Relier();
|
||||
relier.set('uid', 'wibble');
|
||||
user = new User({
|
||||
notifier: notifier,
|
||||
profileClient: profileClient,
|
||||
});
|
||||
account = user.initAccount({
|
||||
email,
|
||||
sessionToken: 'abc123',
|
||||
uid: UID,
|
||||
verified: true,
|
||||
});
|
||||
sinon.stub(account, 'fetchProfile').returns(Promise.resolve());
|
||||
sinon
|
||||
.stub(account, 'fetchActiveSubscriptions')
|
||||
.returns(Promise.resolve([{ id: '123done' }]));
|
||||
sinon.stub(user, 'getAccountByUid').returns(account);
|
||||
sinon.stub(user, 'setSignedInAccountByUid').returns(Promise.resolve());
|
||||
sinon.stub(user, 'getSignedInAccount').returns(account);
|
||||
|
||||
createSupportView();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
$(view.el).remove();
|
||||
view.destroy();
|
||||
view = null;
|
||||
});
|
||||
|
||||
it('should have a header', function() {
|
||||
return view
|
||||
.render()
|
||||
.then(function() {
|
||||
$('#container').append(view.el);
|
||||
})
|
||||
.then(function() {
|
||||
assert.ok(view.$('#fxa-settings-header').length);
|
||||
assert.ok(view.$('#fxa-settings-profile-header-wrapper').length);
|
||||
assert.equal(
|
||||
view.$('#fxa-settings-profile-header-wrapper h1').text(),
|
||||
email
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('submit button', function() {
|
||||
it('should be disabled by default', function() {
|
||||
return view
|
||||
.render()
|
||||
.then(function() {
|
||||
$('#container').append(view.el);
|
||||
})
|
||||
.then(function() {
|
||||
assert.ok(view.$('form button[type=submit]').attr('disabled'));
|
||||
});
|
||||
});
|
||||
|
||||
it('should be enabled once a topic is selected and a message is entered', function() {
|
||||
return view
|
||||
.render()
|
||||
.then(function() {
|
||||
view.afterVisible();
|
||||
$('#container').append(view.el);
|
||||
})
|
||||
.then(function() {
|
||||
view
|
||||
.$('#topic option:eq(1)')
|
||||
.prop('selected', true)
|
||||
.trigger('change');
|
||||
assert.ok(view.$('form button[type=submit]').attr('disabled'));
|
||||
view
|
||||
.$('#message')
|
||||
.val(supportTicket.message)
|
||||
.trigger('keyup');
|
||||
assert.isUndefined(
|
||||
view.$('form button[type=submit]').attr('disabled')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('successful form submission', function() {
|
||||
it('should take user to subscriptions management page', function() {
|
||||
sinon.stub(view, 'navigateToSubscriptionsManagement');
|
||||
sinon
|
||||
.stub(account, 'createSupportTicket')
|
||||
.returns(Promise.resolve({ success: true }));
|
||||
|
||||
return view
|
||||
.render()
|
||||
.then(function() {
|
||||
view.afterVisible();
|
||||
$('#container').append(view.el);
|
||||
})
|
||||
.then(function() {
|
||||
view.$('#topic option:eq(1)').prop('selected', true);
|
||||
view.$('#message').val(supportTicket.message);
|
||||
|
||||
// calling this directly instead of clicking submit so we can have
|
||||
// a promise to await
|
||||
return view.submitSupportForm();
|
||||
})
|
||||
.then(function() {
|
||||
assert.isTrue(account.createSupportTicket.calledOnce);
|
||||
assert.deepEqual(
|
||||
account.createSupportTicket.firstCall.args[0],
|
||||
supportTicket
|
||||
);
|
||||
assert.isTrue(view.navigateToSubscriptionsManagement.calledOnce);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('failed form submission', function() {
|
||||
it('should display an error modal', function() {
|
||||
sinon
|
||||
.stub(account, 'createSupportTicket')
|
||||
.returns(Promise.resolve({ success: false }));
|
||||
|
||||
return view
|
||||
.render()
|
||||
.then(function() {
|
||||
view.afterVisible();
|
||||
$('#container').append(view.el);
|
||||
})
|
||||
.then(function() {
|
||||
view.$('#topic option:eq(1)').prop('selected', true);
|
||||
view.$('#message').val(supportTicket.message);
|
||||
|
||||
// calling this directly instead of clicking submit so we can have
|
||||
// a promise to await
|
||||
return view.submitSupportForm();
|
||||
})
|
||||
.then(function() {
|
||||
assert.ok($('.dialog-error').length);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -157,6 +157,7 @@ require('./spec/views/force_auth');
|
|||
require('./spec/views/form');
|
||||
require('./spec/views/index');
|
||||
require('./spec/views/marketing_snippet');
|
||||
require('./spec/views/mixins/account-by-uid-mixin');
|
||||
require('./spec/views/mixins/account-reset-mixin');
|
||||
require('./spec/views/mixins/avatar-mixin');
|
||||
require('./spec/views/mixins/back-mixin');
|
||||
|
@ -258,6 +259,7 @@ require('./spec/views/sms_sent');
|
|||
require('./spec/views/sub_panels');
|
||||
require('./spec/views/subscriptions_management_redirect');
|
||||
require('./spec/views/subscriptions_product_redirect');
|
||||
require('./spec/views/support');
|
||||
require('./spec/views/tooltip');
|
||||
require('./spec/views/tos');
|
||||
require('./spec/views/why_connect_another_device');
|
||||
|
|
|
@ -2949,6 +2949,11 @@
|
|||
"upath": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"chosen-js": {
|
||||
"version": "1.8.7",
|
||||
"resolved": "https://registry.npmjs.org/chosen-js/-/chosen-js-1.8.7.tgz",
|
||||
"integrity": "sha512-eVdrZJ2U5ISdObkgsi0od5vIJdLwq1P1Xa/Vj/mgxkMZf14DlgobfB6nrlFi3kW4kkvKLsKk4NDqZj1MU1DCpw=="
|
||||
},
|
||||
"chownr": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz",
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
"body-parser": "1.18.2",
|
||||
"cache-loader": "^3.0.1",
|
||||
"celebrate": "7.0.3",
|
||||
"chosen-js": "1.8.7",
|
||||
"connect-cachify": "0.0.17",
|
||||
"consolidate": "0.14.5",
|
||||
"convict": "1.5.0",
|
||||
|
|
|
@ -75,6 +75,7 @@ module.exports = function() {
|
|||
'sms/why',
|
||||
'subscriptions',
|
||||
'subscriptions/products/[\\w_]+',
|
||||
'support',
|
||||
'verify_email',
|
||||
'verify_primary_email',
|
||||
'verify_secondary_email',
|
||||
|
|
|
@ -67,6 +67,7 @@ module.exports = [
|
|||
'tests/functional/sign_in_token_code.js',
|
||||
'tests/functional/sign_in_totp.js',
|
||||
'tests/functional/sign_up.js',
|
||||
'tests/functional/support.js',
|
||||
'tests/functional/sync_v1.js',
|
||||
'tests/functional/sync_v2.js',
|
||||
'tests/functional/sync_v3_email_first.js',
|
||||
|
|
|
@ -31,6 +31,7 @@ const SIGNIN_URL = config.fxaContentRoot + 'signin';
|
|||
const SIGNUP_URL = config.fxaContentRoot + 'signup';
|
||||
const ENABLE_TOTP_URL = `${SETTINGS_URL}/two_step_authentication`;
|
||||
const UNTRUSTED_OAUTH_APP = config.fxaUntrustedOauthApp;
|
||||
const TEST_PRODUCT_URL = `${config.fxaContentRoot}subscriptions/products/${config.testProductId}`;
|
||||
|
||||
/**
|
||||
* Convert a function to a form that can be used as a `then` callback.
|
||||
|
@ -2342,6 +2343,34 @@ const destroySessionForEmail = thenify(function(email) {
|
|||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Subscribe to the test product. The user should be signed in at this point.
|
||||
*
|
||||
* @returns {promise} resolves when complete
|
||||
*/
|
||||
const subscribeToTestProduct = thenify(function() {
|
||||
const nextYear = (new Date().getFullYear() + 1).toString().substr(2);
|
||||
return this.parent
|
||||
.then(openPage(TEST_PRODUCT_URL, 'div.product-payment'))
|
||||
.then(type('input[name=name]', 'Testo McTestson'))
|
||||
.switchToFrame(2)
|
||||
.then(type('input[name=cardnumber]', '4242 4242 4242 4242'))
|
||||
.switchToParentFrame()
|
||||
.end(Infinity)
|
||||
.switchToFrame(3)
|
||||
.then(type('input[name=exp-date]', `12${nextYear}`))
|
||||
.switchToParentFrame()
|
||||
.end(Infinity)
|
||||
.switchToFrame(4)
|
||||
.then(type('.InputElement', '123'))
|
||||
.switchToParentFrame()
|
||||
.end(Infinity)
|
||||
.then(type('input[name=zip]', '12345'))
|
||||
.then(click('input[type=checkbox]'))
|
||||
.then(click('button[name=submit]'))
|
||||
.then(testElementExists('.subscription-ready'));
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
cleanMemory,
|
||||
clearBrowserNotifications,
|
||||
|
@ -2412,6 +2441,7 @@ module.exports = {
|
|||
reOpenWithAdditionalQueryParams,
|
||||
respondToWebChannelMessage,
|
||||
storeWebChannelMessageData,
|
||||
subscribeToTestProduct,
|
||||
switchToWindow,
|
||||
takeScreenshot,
|
||||
testAreEventsLogged,
|
||||
|
|
|
@ -72,6 +72,7 @@ var pages = [
|
|||
'sms/sent',
|
||||
'sms/sent/why',
|
||||
'sms/why',
|
||||
'support',
|
||||
'verify_email',
|
||||
'v1/complete_reset_password',
|
||||
'v1/reset_password',
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
'use strict';
|
||||
|
||||
const { registerSuite } = intern.getInterface('object');
|
||||
const TestHelpers = require('../lib/helpers');
|
||||
const FunctionalHelpers = require('./lib/helpers');
|
||||
|
||||
const config = intern._config;
|
||||
const SIGNIN_URL = config.fxaContentRoot + 'signin';
|
||||
const SUPPORT_URL = config.fxaContentRoot + 'support';
|
||||
const PASSWORD = 'amazingpassword';
|
||||
const email = TestHelpers.createEmail();
|
||||
|
||||
const {
|
||||
clearBrowserState,
|
||||
click,
|
||||
createUser,
|
||||
fillOutSignIn,
|
||||
openPage,
|
||||
subscribeToTestProduct,
|
||||
testElementExists,
|
||||
type,
|
||||
} = FunctionalHelpers;
|
||||
|
||||
registerSuite('support form without valid session', {
|
||||
tests: {
|
||||
'go to support form, redirects to signin': function() {
|
||||
return this.remote.then(openPage(SUPPORT_URL, '#fxa-signin-header'));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registerSuite('support form without active subscriptions', {
|
||||
before: function() {
|
||||
return this.remote
|
||||
.then(createUser(email, PASSWORD, { preVerified: true }))
|
||||
.then(clearBrowserState())
|
||||
.then(openPage(SIGNIN_URL, '#fxa-signin-header'))
|
||||
.then(fillOutSignIn(email, PASSWORD))
|
||||
.then(testElementExists('#fxa-settings-header'));
|
||||
},
|
||||
tests: {
|
||||
'go to support form, redirects to subscription management': function() {
|
||||
return this.remote.then(
|
||||
openPage(SUPPORT_URL, '.subscription-management')
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registerSuite('support form', {
|
||||
before: function() {
|
||||
return this.remote.then(subscribeToTestProduct());
|
||||
},
|
||||
tests: {
|
||||
'go to support form, successfully submits the form': function() {
|
||||
return this.remote
|
||||
.then(openPage(SUPPORT_URL, 'div.support'))
|
||||
.then(click('a.chosen-single'))
|
||||
.then(click('ul.chosen-results li[data-option-array-index="1"]'))
|
||||
.then(type('textarea[name=message]', 'please send halp'))
|
||||
.then(click('button[type=submit]'));
|
||||
// Since we don't have proper Zendesk config in CircleCI, the form
|
||||
// cannot be successfully submitted.
|
||||
// .then(testElementExists('.subscription-management'));
|
||||
},
|
||||
},
|
||||
});
|
|
@ -23,6 +23,7 @@ const fxaEmailRoot = args.fxaEmailRoot || 'http://127.0.0.1:9001';
|
|||
const fxaOAuthApp = args.fxaOAuthApp || 'http://127.0.0.1:8080/';
|
||||
const fxaUntrustedOauthApp =
|
||||
args.fxaUntrustedOauthApp || 'http://127.0.0.1:10139/';
|
||||
const fxaPaymentsRoot = args.fxaPaymentsRoot || 'http://127.0.0.1:3031/';
|
||||
|
||||
// "fxaProduction" is a little overloaded in how it is used in the tests.
|
||||
// Sometimes it means real "stage" or real production configuration, but
|
||||
|
@ -39,6 +40,8 @@ const asyncTimeout = parseInt(args.asyncTimeout || 5000, 10);
|
|||
// args.bailAfterFirstFailure comes in as a string.
|
||||
const bailAfterFirstFailure = args.bailAfterFirstFailure === 'true';
|
||||
|
||||
const testProductId = '123doneProProduct';
|
||||
|
||||
// Intern specific options are here: https://theintern.io/docs.html#Intern/4/docs/docs%2Fconfiguration.md/properties
|
||||
const config = {
|
||||
asyncTimeout: asyncTimeout,
|
||||
|
@ -61,6 +64,7 @@ const config = {
|
|||
fxaToken: fxaToken,
|
||||
fxaTokenRoot: fxaTokenRoot,
|
||||
fxaUntrustedOauthApp: fxaUntrustedOauthApp,
|
||||
fxaPaymentsRoot,
|
||||
|
||||
pageLoadTimeout: 20000,
|
||||
reporters: 'runner',
|
||||
|
@ -70,6 +74,8 @@ const config = {
|
|||
tunnelOptions: {
|
||||
drivers: ['firefox'],
|
||||
},
|
||||
|
||||
testProductId,
|
||||
};
|
||||
|
||||
if (args.grep) {
|
||||
|
|
|
@ -81,6 +81,7 @@ var routes = {
|
|||
'/sms/sent': { statusCode: 200 },
|
||||
'/sms/sent/why': { statusCode: 200 },
|
||||
'/sms/why': { statusCode: 200 },
|
||||
'/support': { statusCode: 200 },
|
||||
// the following have a version prefix
|
||||
'/v1/complete_reset_password': { statusCode: 200 },
|
||||
'/v1/reset_password': { statusCode: 200 },
|
||||
|
|
|
@ -18,6 +18,7 @@ const webpackConfig = {
|
|||
'lib/jquery',
|
||||
'backbone',
|
||||
'canvasToBlob',
|
||||
'chosen-js',
|
||||
'cocktail-lib',
|
||||
'duration',
|
||||
'es6-promise',
|
||||
|
@ -59,6 +60,10 @@ const webpackConfig = {
|
|||
__dirname,
|
||||
'node_modules/blueimp-canvas-to-blob/js/canvas-to-blob'
|
||||
),
|
||||
'chosen-js': path.resolve(
|
||||
__dirname,
|
||||
'node_modules/chosen-js/chosen.jquery'
|
||||
),
|
||||
'cocktail-lib': path.resolve(
|
||||
__dirname,
|
||||
'node_modules/backbone.cocktail/Cocktail'
|
||||
|
|
|
@ -2,10 +2,10 @@ import React, { ReactNode } from 'react';
|
|||
import classNames from 'classnames';
|
||||
import { useClickOutsideEffect } from '../lib/hooks';
|
||||
|
||||
import './DialogMessage.scss';
|
||||
|
||||
import Portal from './Portal';
|
||||
|
||||
import './DialogMessage.scss';
|
||||
|
||||
type DialogMessageProps = {
|
||||
className?: string,
|
||||
onDismiss: Function,
|
||||
|
@ -30,4 +30,4 @@ export const DialogMessage = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default DialogMessage;
|
||||
export default DialogMessage;
|
||||
|
|
|
@ -6,6 +6,7 @@ export interface Config {
|
|||
export interface QueryParams {
|
||||
plan?: string,
|
||||
activated?: string
|
||||
successfulSupportTicketSubmission?: string
|
||||
};
|
||||
|
||||
export interface GenericObject {
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import React from 'react';
|
||||
import { render, cleanup } from '@testing-library/react';
|
||||
import 'jest-dom/extend-expect';
|
||||
import { Subscriptions, SubscriptionsProps } from './index';
|
||||
import { AppContext, AppContextType, defaultAppContext } from '../../lib/AppContext';
|
||||
import { Customer, Profile, Subscription } from '../../store/types';
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
});
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const mockProfile: Profile = {
|
||||
amrValues: [],
|
||||
avatar: 'avatar',
|
||||
avatarDefault: true,
|
||||
displayName: null,
|
||||
email: 'email',
|
||||
locale: 'locale',
|
||||
twoFactorAuthentication: false,
|
||||
uid: 'uid',
|
||||
};
|
||||
const mockCustomer: Customer = {
|
||||
payment_type: 'cc',
|
||||
last4: '4444',
|
||||
exp_month: '12',
|
||||
exp_year: '22',
|
||||
subscriptions: [],
|
||||
};
|
||||
const mockSubscription: Subscription = {
|
||||
'subscriptionId': 'abc',
|
||||
'cancelledAt': null,
|
||||
'createdAt': Date.now(),
|
||||
'productName': 'pro jest',
|
||||
};
|
||||
const mockPlan = {
|
||||
plan_id: 'abc',
|
||||
plan_name: 'abc',
|
||||
product_id: 'abc',
|
||||
product_name: 'abc',
|
||||
currency: 'abc',
|
||||
amount: 100,
|
||||
interval: 'abc',
|
||||
};
|
||||
const mockCustomerSubscription = {
|
||||
cancel_at_period_end: false,
|
||||
current_period_end: 99,
|
||||
current_period_start: 9,
|
||||
end_at: null,
|
||||
nickname: 'abc',
|
||||
plan_id: 'abc',
|
||||
status: 'abc',
|
||||
subscription_id: 'abc',
|
||||
}
|
||||
const mockedSubscriptionsProps = {
|
||||
profile: {error: null, loading: false, result: mockProfile},
|
||||
plans: {error: null, loading: false, result: [mockPlan]},
|
||||
customer: {error: null, loading: false, result: mockCustomer},
|
||||
subscriptions: {error: null, loading: false, result: [mockSubscription]},
|
||||
customerSubscriptions: [mockCustomerSubscription],
|
||||
fetchSubscriptionsRouteResources: jest.fn(),
|
||||
cancelSubscription: jest.fn(),
|
||||
cancelSubscriptionStatus: {error: null, loading: false, result: mockSubscription},
|
||||
resetCancelSubscription: jest.fn(),
|
||||
reactivateSubscription: jest.fn(),
|
||||
reactivateSubscriptionStatus: {error: null, loading: false, result: null},
|
||||
resetReactivateSubscription: jest.fn(),
|
||||
updatePayment: jest.fn(),
|
||||
updatePaymentStatus: {error: null, loading: false, result: null},
|
||||
resetUpdatePayment: jest.fn(),
|
||||
};
|
||||
const Subject = (props: SubscriptionsProps) => {
|
||||
const appContextValue = {
|
||||
...defaultAppContext,
|
||||
config: {servers: {content: {url: 'http://127.0.0.1:3030'}}},
|
||||
queryParams: { successfulSupportTicketSubmission: 'quux'},
|
||||
};
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={appContextValue}>
|
||||
<Subscriptions {...props} />
|
||||
</AppContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
it('renders successful support ticket submission messsage when query param exists', () => {
|
||||
const { getByTestId } = render(<Subject {...mockedSubscriptionsProps} />);
|
||||
expect(getByTestId('supportFormSuccess')).toBeInTheDocument();
|
||||
});
|
|
@ -29,9 +29,6 @@ import DialogMessage from '../../components/DialogMessage';
|
|||
import SubscriptionItem from './SubscriptionItem';
|
||||
import { LoadingOverlay } from '../../components/LoadingOverlay';
|
||||
|
||||
// TODO: From where does this URL come - "Contact Support" button destination.
|
||||
const SUPPORT_PANEL_URL = 'https://support.accounts.firefox.com';
|
||||
|
||||
export type SubscriptionsProps = {
|
||||
profile: ProfileFetchState,
|
||||
plans: PlansFetchState,
|
||||
|
@ -68,13 +65,16 @@ export const Subscriptions = ({
|
|||
}: SubscriptionsProps) => {
|
||||
const {
|
||||
accessToken,
|
||||
config,
|
||||
locationReload,
|
||||
navigateToUrl,
|
||||
queryParams
|
||||
} = useContext(AppContext);
|
||||
|
||||
const [showPaymentSuccessAlert, setShowPaymentSuccessAlert] = useState(true);
|
||||
const clearSuccessAlert = useCallback(() => setShowPaymentSuccessAlert(false),
|
||||
[setShowPaymentSuccessAlert]);
|
||||
const SUPPORT_FORM_URL = `${config.servers.content.url}/support`;
|
||||
|
||||
// Fetch subscriptions and customer on initial render or auth change.
|
||||
useEffect(() => {
|
||||
|
@ -84,8 +84,8 @@ export const Subscriptions = ({
|
|||
}, [ fetchSubscriptionsRouteResources, accessToken ]);
|
||||
|
||||
const onSupportClick = useCallback(
|
||||
() => navigateToUrl(SUPPORT_PANEL_URL),
|
||||
[ navigateToUrl ]
|
||||
() => navigateToUrl(SUPPORT_FORM_URL),
|
||||
[ navigateToUrl, SUPPORT_FORM_URL ]
|
||||
);
|
||||
|
||||
if (customer.loading || subscriptions.loading || profile.loading || plans.loading) {
|
||||
|
@ -140,7 +140,8 @@ export const Subscriptions = ({
|
|||
subscription: cancelSubscriptionStatus.result,
|
||||
customerSubscriptions,
|
||||
plans,
|
||||
resetCancelSubscription
|
||||
resetCancelSubscription,
|
||||
supportFormUrl: SUPPORT_FORM_URL
|
||||
}} />
|
||||
)}
|
||||
|
||||
|
@ -175,6 +176,15 @@ export const Subscriptions = ({
|
|||
</DialogMessage>
|
||||
)}
|
||||
|
||||
{queryParams.successfulSupportTicketSubmission && (
|
||||
<AlertBar className="alert alertSuccess">
|
||||
<span data-testid="supportFormSuccess">
|
||||
Your support question was sent! We'll reach out to you via email as soon as possible.
|
||||
</span>
|
||||
</AlertBar>
|
||||
)}
|
||||
|
||||
|
||||
{profile.result && ( <ProfileBanner profile={profile.result} /> )}
|
||||
|
||||
<div className="child-views">
|
||||
|
@ -268,6 +278,7 @@ type CancellationDialogMessageProps = {
|
|||
customerSubscriptions: Array<CustomerSubscription>,
|
||||
plans: PlansFetchState,
|
||||
resetCancelSubscription: Function,
|
||||
supportFormUrl: string
|
||||
};
|
||||
|
||||
const CancellationDialogMessage = ({
|
||||
|
@ -275,6 +286,7 @@ const CancellationDialogMessage = ({
|
|||
customerSubscriptions,
|
||||
plans,
|
||||
resetCancelSubscription,
|
||||
supportFormUrl,
|
||||
}: CancellationDialogMessageProps) => {
|
||||
const customerSubscription = customerSubscriptionForId(
|
||||
subscription.subscriptionId,
|
||||
|
@ -298,7 +310,7 @@ const CancellationDialogMessage = ({
|
|||
You will still have until access to {plan.plan_name} until {periodEndDate}.
|
||||
</p>
|
||||
<p className="small">
|
||||
Have questions? Visit <a href={SUPPORT_PANEL_URL}>Mozilla Support</a>.
|
||||
Have questions? Visit <a href={supportFormUrl}>Mozilla Support</a>.
|
||||
</p>
|
||||
</DialogMessage>
|
||||
);
|
||||
|
|
Загрузка…
Ссылка в новой задаче