Skip to content

Commit e3a7015

Browse files
authored
New user journey dev (#2766)
* New payment details implementation * Fix tests * Antifraud for v1 plans * Adjust tests * fix for lack of first_sync for assembla * added hosted billing help * Add the new macOS information section in select-plan and modify billing scss styles * Adjust manual and github plans view * Fix local registration checkbox * ui updates * redirection attempt, wizard fix, trial plan desc * ui updates * sync subscriptions on first_syn * redirect from ghapp installation to firstsync * style fixes,profile menu update,fix for company * tests update * ui updates * ui updates * redirection updates, typos fixed * ui updates - plan, wizard, activation * coupon * installation redirections, email banner update * random ui updates * lint * total price hide * empty invoices field, installation_id fix * radio color,mail banner, double badge * ui fixes - review 03.28 * sync popup improvement * activation button fix * lint
1 parent d9d5bc4 commit e3a7015

98 files changed

Lines changed: 5521 additions & 893 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/adapters/user.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default V3Adapter.extend({
88

99
assert('Invalid parameters for /user request', isQueryingCurrentUser || isUpdatingCurrentUser);
1010

11-
return `${this.urlPrefix()}/user`;
11+
return `${this.urlPrefix()}/user?include=user.collaborator`;
1212
},
1313

1414
// This overrides the parent implementation to ignore the query parameters
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import Component from '@ember/component';
2+
import { task } from 'ember-concurrency';
3+
import { inject as service } from '@ember/service';
4+
import { not, reads, filterBy, alias } from '@ember/object/computed';
5+
import { computed } from '@ember/object';
6+
import config from 'travis/config/environment';
7+
import { countries, states, zeroVatThresholdCountries, nonZeroVatThresholdCountries, stateCountries } from 'travis/utils/countries';
8+
9+
export default Component.extend({
10+
stripe: service(),
11+
store: service(),
12+
auth: service(),
13+
accounts: service(),
14+
flashes: service(),
15+
metrics: service(),
16+
storage: service(),
17+
router: service(),
18+
wizard: service('wizard-state'),
19+
countries,
20+
user: null,
21+
account: alias('accounts.user'),
22+
stripeElement: null,
23+
stripeLoading: false,
24+
couponId: null,
25+
options: config.stripeOptions,
26+
showSwitchToFreeModal: false,
27+
showPlanSwitchWarning: false,
28+
availablePlans: reads('account.eligibleV2Plans'),
29+
defaultPlans: filterBy('availablePlans', 'trialPlan'),
30+
defaultPlanId: reads('defaultPlans.firstObject.id'),
31+
showCancelButton: false,
32+
travisTermsUrl: 'https://www.ideracorp.com/Legal/PrivacyShield',
33+
travisPolicyUrl: 'https://www.ideracorp.com/Legal/PrivacyShield',
34+
subscription: null,
35+
vatId: null,
36+
37+
displayedPlans: reads('availablePlans'),
38+
39+
selectedPlan: computed('displayedPlans.[].id', 'defaultPlanId', function () {
40+
let plan = this.storage.selectedPlanId;
41+
if (plan == null) {
42+
plan = this.defaultPlanId;
43+
}
44+
45+
return this.displayedPlans.findBy('id', plan);
46+
}),
47+
48+
isTrial: computed('selectedPlan', function () {
49+
let plan = this.selectedPlan;
50+
return plan ? plan.isTrial : true;
51+
}),
52+
53+
hasLocalRegistration: false,
54+
firstName: '',
55+
lastName: '',
56+
company: '',
57+
address: '',
58+
city: '',
59+
country: '',
60+
billingEmail: '',
61+
billingEmails: computed('billingEmail', function () {
62+
return (this.billingEmail || '').split(',');
63+
}),
64+
65+
states: computed('country', function () {
66+
const { country } = this;
67+
68+
return states[country];
69+
}),
70+
71+
isStateCountry: computed('country', function () {
72+
const { country } = this;
73+
74+
return !!country && stateCountries.includes(country);
75+
}),
76+
77+
isZeroVatThresholdCountry: computed('country', function () {
78+
const { country } = this;
79+
return !!country && zeroVatThresholdCountries.includes(country);
80+
}),
81+
82+
isNonZeroVatThresholdCountry: computed('country', function () {
83+
const { country } = this;
84+
return !!country && nonZeroVatThresholdCountries.includes(country);
85+
}),
86+
87+
isVatMandatory: computed('isNonZeroVatThresholdCountry', 'hasLocalRegistration', function () {
88+
const { isNonZeroVatThresholdCountry, isZeroVatThresholdCountry, hasLocalRegistration } = this;
89+
return isZeroVatThresholdCountry || (isNonZeroVatThresholdCountry ? hasLocalRegistration : false);
90+
}),
91+
92+
showNonZeroVatConfirmation: reads('isNonZeroVatThresholdCountry'),
93+
94+
showVatField: computed('country', 'isNonZeroVatThresholdCountry', 'hasLocalRegistration', function () {
95+
const { country, isNonZeroVatThresholdCountry, hasLocalRegistration } = this;
96+
return country && (isNonZeroVatThresholdCountry ? hasLocalRegistration : true);
97+
}),
98+
99+
isStateMandatory: reads('isStateCountry'),
100+
101+
isLoading: false,
102+
planDetailsVisible: false,
103+
104+
isNewSubscription: not('subscription.id'),
105+
106+
creditCardInfo: null,
107+
creditCardOwner: null,
108+
109+
creditCardInfoEmpty: computed('subscription.creditCardInfo', function () {
110+
return !this.creditCardInfo.lastDigits;
111+
}),
112+
113+
getPriceInfo: computed('selectedPlan', function () {
114+
let plan = this.selectedPlan;
115+
return `$${plan.startingPrice} ${(plan.isAnnual ? ' annualy' : ' monthly')}`;
116+
}),
117+
118+
getActivateButtonText: computed('selectedPlan', function () {
119+
let text = 'Verify Your Account';
120+
let plan = this.selectedPlan;
121+
if (plan && !plan.isTrial) {
122+
text = `Activate ${plan.name}`;
123+
}
124+
return text;
125+
}),
126+
127+
canActivate: computed('country', 'zipCode', 'address', 'creditCardOwner', 'city', 'stripeElement', 'billingEmail', function () {
128+
let valid = (val) => !(val === null || val.trim() === '');
129+
return valid(this.billingEmail) && valid(this.country) &&
130+
valid(this.zipCode) && valid(this.address) &&
131+
valid(this.creditCardOwner) && this.stripeElement &&
132+
valid(this.city);
133+
}),
134+
135+
createSubscription: task(function* () {
136+
this.metrics.trackEvent({
137+
action: 'Pay Button Clicked',
138+
category: 'Subscription',
139+
});
140+
const { stripeElement, selectedPlan } = this;
141+
try {
142+
this.set('subscription', this.newV2Subscription());
143+
const { token } = yield this.stripe.createStripeToken.perform(stripeElement);
144+
if (token) {
145+
const organizationId = null;
146+
const plan = selectedPlan && selectedPlan.id && this.store.peekRecord('v2-plan-config', selectedPlan.id);
147+
const org = organizationId && this.store.peekRecord('organization', organizationId);
148+
149+
this.subscription.setProperties({
150+
organization: org,
151+
plan: plan,
152+
v1SubscriptionId: this.v1SubscriptionId,
153+
});
154+
if (!this.subscription.id) {
155+
this.subscription.creditCardInfo.setProperties({
156+
token: token.id,
157+
lastDigits: token.card.last4
158+
});
159+
this.subscription.setProperties({
160+
coupon: this.couponId
161+
});
162+
const { clientSecret } = yield this.subscription.save();
163+
yield this.stripe.handleStripePayment.perform(clientSecret);
164+
} else {
165+
yield this.subscription.creditCardInfo.updateToken.perform({
166+
subscriptionId: this.subscription.id,
167+
tokenId: token.id,
168+
tokenCard: token.card
169+
});
170+
yield this.subscription.save();
171+
yield this.subscription.changePlan.perform(selectedPlan.id, this.couponId);
172+
yield this.accounts.fetchV2Subscriptions.perform();
173+
yield this.retryAuthorization.perform();
174+
}
175+
this.metrics.trackEvent({ button: 'pay-button' });
176+
this.storage.clearBillingData();
177+
this.storage.clearSelectedPlanId();
178+
this.storage.wizardStep = 2;
179+
this.wizard.update.perform(2);
180+
yield this.accounts.fetchV2Subscriptions.perform().then(() => {
181+
this.router.transitionTo('/account/repositories');
182+
});
183+
}
184+
this.flashes.success('Your account has been successfully activated');
185+
} catch (error) {
186+
yield this.accounts.fetchV2Subscriptions.perform().then(() => {
187+
if (this.accounts.user.subscription || this.accounts.user.v2subscription) {
188+
this.storage.clearBillingData();
189+
this.storage.clearSelectedPlanId();
190+
this.storage.wizardStep = 2;
191+
this.wizard.update.perform(2);
192+
this.router.transitionTo('account.repositories');
193+
} else {
194+
this.handleError();
195+
}
196+
});
197+
}
198+
}).drop(),
199+
200+
newV2Subscription() {
201+
const plan = this.store.createRecord('v2-plan-config');
202+
const billingInfo = this.store.createRecord('v2-billing-info');
203+
const creditCardInfo = this.store.createRecord('v2-credit-card-info');
204+
let ownerName = this.creditCardOwner.trim();
205+
let idx = ownerName.lastIndexOf(' ');
206+
if (idx > 0) {
207+
this.firstName = ownerName.substr(0, idx);
208+
this.lastName = ownerName.substr(idx + 1);
209+
} else {
210+
this.firstName = '';
211+
this.lastName = ownerName;
212+
}
213+
let empty = (val) => val === null || val.trim() === '';
214+
if (empty(this.lastName) || empty(this.address) ||
215+
empty(this.city) || empty(this.zipCode) ||
216+
empty(this.country) || empty(this.billingEmail)
217+
) {
218+
throw new Error('Fill all required fields');
219+
}
220+
billingInfo.setProperties({
221+
firstName: this.firstName,
222+
lastName: this.lastName,
223+
address: this.address,
224+
city: this.city,
225+
company: this.company,
226+
zipCode: this.zipCode,
227+
country: this.country,
228+
state: this.state,
229+
billingEmail: this.billingEmail,
230+
hasLocalRegistration: this.hasLocalRegistration,
231+
vatId: this.vatId
232+
});
233+
creditCardInfo.setProperties({
234+
token: '',
235+
lastDigits: ''
236+
});
237+
return this.store.createRecord('v2-subscription', {
238+
billingInfo,
239+
plan,
240+
creditCardInfo,
241+
});
242+
},
243+
handleError() {
244+
let message = this.get('selectedPlan.isTrial')
245+
? 'Credit card verification failed, please try again or use a different card.'
246+
: 'An error occurred when creating your subscription. Please try again.';
247+
this.flashes.error(message);
248+
},
249+
250+
validateCoupon: task(function* () {
251+
return yield this.store.findRecord('coupon', this.couponId, {
252+
reload: true,
253+
});
254+
}).drop(),
255+
256+
coupon: reads('validateCoupon.last.value'),
257+
couponError: reads('validateCoupon.last.error'),
258+
isValidCoupon: reads('coupon.valid'),
259+
couponHasError: computed('couponError', {
260+
get() {
261+
return !!this.couponError;
262+
},
263+
set(key, value) {
264+
return value;
265+
}
266+
}),
267+
268+
actions: {
269+
complete(stripeElement) {
270+
this.set('stripeElement', stripeElement);
271+
},
272+
handleCouponFocus() {
273+
this.set('couponHasError', false);
274+
},
275+
276+
clearCreditCardData() {
277+
this.subscription.set('creditCardInfo', null);
278+
},
279+
changePlan() {
280+
this.set('showPlansSelector', true);
281+
},
282+
closePlansModal() {
283+
this.set('showPlansSelector', false);
284+
},
285+
verifyAccount() {
286+
287+
},
288+
subscribe() {
289+
if (this.canActivate) {
290+
this.createSubscription.perform();
291+
}
292+
},
293+
changeCountry(country) {
294+
this.set('country', country);
295+
this.hasLocalRegistration = false;
296+
},
297+
togglePlanDetails() {
298+
this.set('planDetailsVisible', !this.planDetailsVisible);
299+
}
300+
301+
}
302+
});

0 commit comments

Comments
 (0)