diff --git a/packages/fxa-auth-server/lib/routes/support.js b/packages/fxa-auth-server/lib/routes/support.js
index 9371036bcf..209136f3ec 100644
--- a/packages/fxa-auth-server/lib/routes/support.js
+++ b/packages/fxa-auth-server/lib/routes/support.js
@@ -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(),
}),
},
diff --git a/packages/fxa-content-server/app/scripts/lib/payment-server.js b/packages/fxa-content-server/app/scripts/lib/payment-server.js
index bdf06ab26e..dab91ed2c5 100644
--- a/packages/fxa-content-server/app/scripts/lib/payment-server.js
+++ b/packages/fxa-content-server/app/scripts/lib/payment-server.js
@@ -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);
diff --git a/packages/fxa-content-server/app/scripts/lib/router.js b/packages/fxa-content-server/app/scripts/lib/router.js
index 78466b5a13..c8f8d6b67a 100644
--- a/packages/fxa-content-server/app/scripts/lib/router.js
+++ b/packages/fxa-content-server/app/scripts/lib/router.js
@@ -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,
}),
diff --git a/packages/fxa-content-server/app/scripts/templates/partial/support-form-error.mustache b/packages/fxa-content-server/app/scripts/templates/partial/support-form-error.mustache
new file mode 100644
index 0000000000..0f820980e1
--- /dev/null
+++ b/packages/fxa-content-server/app/scripts/templates/partial/support-form-error.mustache
@@ -0,0 +1,5 @@
+
+
+
{{#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}}
+
+
diff --git a/packages/fxa-content-server/app/scripts/templates/support.mustache b/packages/fxa-content-server/app/scripts/templates/support.mustache
new file mode 100644
index 0000000000..5ef00ebe16
--- /dev/null
+++ b/packages/fxa-content-server/app/scripts/templates/support.mustache
@@ -0,0 +1,78 @@
+
+
+
+
+
diff --git a/packages/fxa-content-server/app/scripts/views/mixins/account-by-uid-mixin.js b/packages/fxa-content-server/app/scripts/views/mixins/account-by-uid-mixin.js
new file mode 100644
index 0000000000..4af4a06bab
--- /dev/null
+++ b/packages/fxa-content-server/app/scripts/views/mixins/account-by-uid-mixin.js
@@ -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');
+ }
+ },
+};
diff --git a/packages/fxa-content-server/app/scripts/views/settings.js b/packages/fxa-content-server/app/scripts/views/settings.js
index c5d1d42ebc..e88e358694 100644
--- a/packages/fxa-content-server/app/scripts/views/settings.js
+++ b/packages/fxa-content-server/app/scripts/views/settings.js
@@ -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,
diff --git a/packages/fxa-content-server/app/scripts/views/support.js b/packages/fxa-content-server/app/scripts/views/support.js
new file mode 100644
index 0000000000..468ea125c0
--- /dev/null
+++ b/packages/fxa-content-server/app/scripts/views/support.js
@@ -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;
diff --git a/packages/fxa-content-server/app/styles/_modules.scss b/packages/fxa-content-server/app/styles/_modules.scss
index 1c9c9f881e..9385982758 100644
--- a/packages/fxa-content-server/app/styles/_modules.scss
+++ b/packages/fxa-content-server/app/styles/_modules.scss
@@ -17,3 +17,5 @@
@import 'modules/graphic';
@import 'modules/marketing';
@import 'modules/marketing-ios';
+@import 'modules/support';
+@import 'modules/chosen';
diff --git a/packages/fxa-content-server/app/styles/modules/_button-row.scss b/packages/fxa-content-server/app/styles/modules/_button-row.scss
index 390165b332..21ac70117a 100644
--- a/packages/fxa-content-server/app/styles/modules/_button-row.scss
+++ b/packages/fxa-content-server/app/styles/modules/_button-row.scss
@@ -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;
}
}
diff --git a/packages/fxa-content-server/app/styles/modules/_chosen.scss b/packages/fxa-content-server/app/styles/modules/_chosen.scss
new file mode 100644
index 0000000000..e552116c0b
--- /dev/null
+++ b/packages/fxa-content-server/app/styles/modules/_chosen.scss
@@ -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);
+ }
+}
diff --git a/packages/fxa-content-server/app/styles/modules/support.scss b/packages/fxa-content-server/app/styles/modules/support.scss
new file mode 100644
index 0000000000..dea16c30d1
--- /dev/null
+++ b/packages/fxa-content-server/app/styles/modules/support.scss
@@ -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;
+ }
+ }
+}
diff --git a/packages/fxa-content-server/app/tests/spec/lib/payment-server.js b/packages/fxa-content-server/app/tests/spec/lib/payment-server.js
index fbce10f92c..e32c320a66 100644
--- a/packages/fxa-content-server/app/tests/spec/lib/payment-server.js
+++ b/packages/fxa-content-server/app/tests/spec/lib/payment-server.js
@@ -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'
+ );
+ });
+ });
});
diff --git a/packages/fxa-content-server/app/tests/spec/views/mixins/account-by-uid-mixin.js b/packages/fxa-content-server/app/tests/spec/views/mixins/account-by-uid-mixin.js
new file mode 100644
index 0000000000..4b4e81c039
--- /dev/null
+++ b/packages/fxa-content-server/app/tests/spec/views/mixins/account-by-uid-mixin.js
@@ -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();
+ });
+ });
+});
diff --git a/packages/fxa-content-server/app/tests/spec/views/support.js b/packages/fxa-content-server/app/tests/spec/views/support.js
new file mode 100644
index 0000000000..828452ac74
--- /dev/null
+++ b/packages/fxa-content-server/app/tests/spec/views/support.js
@@ -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);
+ });
+ });
+ });
+});
diff --git a/packages/fxa-content-server/app/tests/test_start.js b/packages/fxa-content-server/app/tests/test_start.js
index 4b9f004012..1702c014f9 100644
--- a/packages/fxa-content-server/app/tests/test_start.js
+++ b/packages/fxa-content-server/app/tests/test_start.js
@@ -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');
diff --git a/packages/fxa-content-server/npm-shrinkwrap.json b/packages/fxa-content-server/npm-shrinkwrap.json
index 5f7b057e3d..df55b2f38d 100644
--- a/packages/fxa-content-server/npm-shrinkwrap.json
+++ b/packages/fxa-content-server/npm-shrinkwrap.json
@@ -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",
diff --git a/packages/fxa-content-server/package.json b/packages/fxa-content-server/package.json
index 0cee88f65d..7befe9b6cf 100644
--- a/packages/fxa-content-server/package.json
+++ b/packages/fxa-content-server/package.json
@@ -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",
diff --git a/packages/fxa-content-server/server/lib/routes/get-frontend.js b/packages/fxa-content-server/server/lib/routes/get-frontend.js
index c8a5354578..de9cd749bb 100644
--- a/packages/fxa-content-server/server/lib/routes/get-frontend.js
+++ b/packages/fxa-content-server/server/lib/routes/get-frontend.js
@@ -75,6 +75,7 @@ module.exports = function() {
'sms/why',
'subscriptions',
'subscriptions/products/[\\w_]+',
+ 'support',
'verify_email',
'verify_primary_email',
'verify_secondary_email',
diff --git a/packages/fxa-content-server/tests/functional.js b/packages/fxa-content-server/tests/functional.js
index e533baf606..d9f1b0d792 100644
--- a/packages/fxa-content-server/tests/functional.js
+++ b/packages/fxa-content-server/tests/functional.js
@@ -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',
diff --git a/packages/fxa-content-server/tests/functional/lib/helpers.js b/packages/fxa-content-server/tests/functional/lib/helpers.js
index 01e3be7b31..b5cf6a42d7 100644
--- a/packages/fxa-content-server/tests/functional/lib/helpers.js
+++ b/packages/fxa-content-server/tests/functional/lib/helpers.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,
diff --git a/packages/fxa-content-server/tests/functional/pages.js b/packages/fxa-content-server/tests/functional/pages.js
index b59d08aaca..fdb26bc693 100644
--- a/packages/fxa-content-server/tests/functional/pages.js
+++ b/packages/fxa-content-server/tests/functional/pages.js
@@ -72,6 +72,7 @@ var pages = [
'sms/sent',
'sms/sent/why',
'sms/why',
+ 'support',
'verify_email',
'v1/complete_reset_password',
'v1/reset_password',
diff --git a/packages/fxa-content-server/tests/functional/support.js b/packages/fxa-content-server/tests/functional/support.js
new file mode 100644
index 0000000000..8c662bb906
--- /dev/null
+++ b/packages/fxa-content-server/tests/functional/support.js
@@ -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'));
+ },
+ },
+});
diff --git a/packages/fxa-content-server/tests/intern.js b/packages/fxa-content-server/tests/intern.js
index 0c3d771bad..22b8089114 100644
--- a/packages/fxa-content-server/tests/intern.js
+++ b/packages/fxa-content-server/tests/intern.js
@@ -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) {
diff --git a/packages/fxa-content-server/tests/server/routes.js b/packages/fxa-content-server/tests/server/routes.js
index 7fa9d480be..1cbca91f70 100644
--- a/packages/fxa-content-server/tests/server/routes.js
+++ b/packages/fxa-content-server/tests/server/routes.js
@@ -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 },
diff --git a/packages/fxa-content-server/webpack.config.js b/packages/fxa-content-server/webpack.config.js
index ad47cf7136..2929a128c3 100644
--- a/packages/fxa-content-server/webpack.config.js
+++ b/packages/fxa-content-server/webpack.config.js
@@ -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'
diff --git a/packages/fxa-payments-server/src/components/DialogMessage.tsx b/packages/fxa-payments-server/src/components/DialogMessage.tsx
index ade05c1ec1..9f5d58f3b6 100644
--- a/packages/fxa-payments-server/src/components/DialogMessage.tsx
+++ b/packages/fxa-payments-server/src/components/DialogMessage.tsx
@@ -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;
\ No newline at end of file
+export default DialogMessage;
diff --git a/packages/fxa-payments-server/src/lib/types.tsx b/packages/fxa-payments-server/src/lib/types.tsx
index 8e34e90add..4ef9a584ad 100644
--- a/packages/fxa-payments-server/src/lib/types.tsx
+++ b/packages/fxa-payments-server/src/lib/types.tsx
@@ -6,6 +6,7 @@ export interface Config {
export interface QueryParams {
plan?: string,
activated?: string
+ successfulSupportTicketSubmission?: string
};
export interface GenericObject {
diff --git a/packages/fxa-payments-server/src/routes/Subscriptions/index.test.tsx b/packages/fxa-payments-server/src/routes/Subscriptions/index.test.tsx
new file mode 100644
index 0000000000..b4ac1d99ac
--- /dev/null
+++ b/packages/fxa-payments-server/src/routes/Subscriptions/index.test.tsx
@@ -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 (
+
+
+
+ )
+}
+
+it('renders successful support ticket submission messsage when query param exists', () => {
+ const { getByTestId } = render();
+ expect(getByTestId('supportFormSuccess')).toBeInTheDocument();
+});
diff --git a/packages/fxa-payments-server/src/routes/Subscriptions/index.tsx b/packages/fxa-payments-server/src/routes/Subscriptions/index.tsx
index 2bcdea4574..c09a066989 100644
--- a/packages/fxa-payments-server/src/routes/Subscriptions/index.tsx
+++ b/packages/fxa-payments-server/src/routes/Subscriptions/index.tsx
@@ -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 = ({
)}
+ {queryParams.successfulSupportTicketSubmission && (
+
+
+ Your support question was sent! We'll reach out to you via email as soon as possible.
+
+
+ )}
+
+
{profile.result && ( )}
@@ -268,6 +278,7 @@ type CancellationDialogMessageProps = {
customerSubscriptions: Array
,
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}.
- Have questions? Visit Mozilla Support.
+ Have questions? Visit Mozilla Support.
);