Skip to content

Commit b125cde

Browse files
committed
ECTO-2519 - improve IAP checkout subscription change UI (#137)
1 parent 31116fc commit b125cde

8 files changed

Lines changed: 259 additions & 53 deletions

File tree

src/Resources/app/administration/src/module/sw-in-app-purchases/component/sw-in-app-purchase-checkout-overview/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,17 @@ export default Shopware.Component.wrapComponentConfig({
5555
},
5656

5757
computed: {
58+
currencyFilter() {
59+
return Shopware.Filter.getByName('currency');
60+
},
61+
5862
purchaseOptions(): Array<{ value: string; name: string }> {
5963
return this.purchase.priceModels.map((priceModel): { value: string; name: string } => {
64+
const price = String(this.currencyFilter(priceModel.price, 'EUR', 2));
65+
const duration = this.$t(`sw-in-app-purchase-price-box.duration.${priceModel.variant}`);
6066
return {
6167
value: priceModel.variant,
62-
name: `${priceModel.price}* /${this.$t(`sw-in-app-purchase-price-box.duration.${priceModel.variant}`)}`
68+
name: `${price}* /${duration}`
6369
};
6470
});
6571
},

src/Resources/app/administration/src/module/sw-in-app-purchases/component/sw-in-app-purchase-checkout-subscription-change/index.ts

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,50 +28,96 @@ export default Shopware.Component.wrapComponentConfig({
2828
return Shopware.Filter.getByName('currency');
2929
},
3030

31-
formattedStartingDate(): string {
32-
const date = new Date(this.cartPosition.nextBookingDate ?? '');
33-
return date.toLocaleDateString(this.locale, { month: 'numeric', day: 'numeric' });
31+
isNextBookingDateInDifferentYear(): boolean {
32+
const today = new Date();
33+
const nextBookingDate = this.cartPosition?.nextBookingDate;
34+
return nextBookingDate !== null && new Date(nextBookingDate).getFullYear() !== today.getFullYear();
3435
},
3536

36-
infoHint(): string {
37-
const today = new Date().toLocaleDateString(this.locale, {
38-
month: 'long',
39-
day: 'numeric'
40-
});
37+
yearFormat(): 'numeric' | '2-digit' | undefined {
38+
return this.isNextBookingDateInDifferentYear ? 'numeric' : undefined;
39+
},
40+
41+
infoHint(): string | null {
42+
if (!this.cartPosition) {
43+
return null;
44+
}
45+
46+
const variant = this.$t(`sw-in-app-purchase-checkout-subscription-change.variant.${this.cartPosition.variant}`);
4147

42-
const nextBookingDate = this.cartPosition?.nextBookingDate
48+
const nextBookingDate = this.cartPosition.nextBookingDate
4349
? new Date(this.cartPosition.nextBookingDate).toLocaleDateString(this.locale, {
4450
month: 'long',
45-
day: 'numeric'
51+
day: 'numeric',
52+
year: this.yearFormat
4653
})
4754
: '';
4855

49-
return this.$t('sw-in-app-purchase-checkout-subscription-change.info-lint', {
56+
if (this.cartPosition.subscriptionChange?.type === 'downgrade') {
57+
return this.$t('sw-in-app-purchase-checkout-subscription-change.downgrade-hint', {
58+
variant,
59+
fee: this.currencyFilter(this.cart.netPrice, 'EUR', 2),
60+
start: nextBookingDate
61+
});
62+
}
63+
64+
const today = new Date().toLocaleDateString(this.locale, {
65+
month: 'long',
66+
day: 'numeric',
67+
year: this.yearFormat
68+
});
69+
70+
return this.$t('sw-in-app-purchase-checkout-subscription-change.upgrade-hint', {
5071
today,
51-
price: this.currencyFilter(this.cartPosition?.proratedNetPrice, 'EUR', 2),
52-
variant: this.cartPosition?.variant ?? '',
72+
price: this.currencyFilter(this.cartPosition.proratedNetPrice, 'EUR', 2),
73+
variant,
5374
fee: this.currencyFilter(this.cart.netPrice, 'EUR', 2),
5475
start: nextBookingDate
5576
});
5677
},
5778

58-
cartPosition() {
79+
cartPosition(): IAP.InAppPurchaseCartPosition {
5980
return this.cart.positions[0];
6081
},
6182

62-
isIncludedInPluginLicense() {
83+
isIncludedInPluginLicense(): boolean {
6384
return this.cartPosition?.subscriptionChange?.isIncludedInPluginLicense ?? false;
6485
},
6586

66-
getCurrentPrice() {
67-
const price = this.cartPosition?.subscriptionChange?.currentFeature?.priceModels
68-
?.find((priceModel) => priceModel.variant === this.cartPosition.variant)?.price;
87+
currentPrice(): string {
88+
return String(this.currencyFilter(this.cartPosition?.subscriptionChange?.currentNetPrice, 'EUR', 2));
89+
},
90+
91+
currentPlanName(): string {
92+
return this.cartPosition?.subscriptionChange?.currentFeature.name ?? '';
93+
},
94+
95+
currentPlanDuration(): string | null {
96+
if (!this.cartPosition?.subscriptionChange?.currentFeatureVariant) {
97+
return null;
98+
}
99+
100+
return this.$t(`sw-in-app-purchase-price-box.duration.${this.cartPosition?.subscriptionChange?.currentFeatureVariant}`);
101+
},
69102

70-
return String(this.currencyFilter(price, 'EUR', 2));
103+
newPlanName(): string {
104+
return this.cartPosition?.feature.name ?? '';
71105
},
72106

73-
getCurrentPlanName() {
74-
return this.cartPosition?.subscriptionChange?.currentFeature?.name;
107+
newPlanDuration(): string | null {
108+
if (!this.cartPosition?.variant) {
109+
return null;
110+
}
111+
112+
return this.$t(`sw-in-app-purchase-price-box.duration.${this.cartPosition?.variant}`);
113+
},
114+
115+
proratedNetPrice(): string {
116+
if (this.cartPosition?.subscriptionChange?.type === 'downgrade') {
117+
return String(this.currencyFilter(0.0, 'EUR', 2));
118+
}
119+
120+
return String(this.currencyFilter(this.cartPosition?.proratedNetPrice, 'EUR', 2));
75121
}
76122
}
77123
});

src/Resources/app/administration/src/module/sw-in-app-purchases/component/sw-in-app-purchase-checkout-subscription-change/sw-in-app-purchase-checkout-subscription-change.html.twig

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,30 @@
22
<div class="sw-in-app-purchase-checkout-subscription-change__item">
33
<p class="sw-in-app-purchase-checkout-subscription-change__plan-name">
44
{{ $t('sw-in-app-purchase-checkout-subscription-change.current-plan') }}
5+
<span class="sw-in-app-purchase-checkout-subscription-change__current-plan">({{ currentPlanName }})</span>
56
</p>
67
<p class="sw-in-app-purchase-checkout-subscription-change__plan-price">
78
<template v-if="isIncludedInPluginLicense">
89
{{ $t('sw-in-app-purchase-checkout-subscription-change.free') }}
910
</template>
1011
<template v-else>
11-
{{ getCurrentPrice }}*/{{ $t('sw-in-app-purchase-price-box.duration.' + cartPosition.variant) }}
12+
{{ currentPrice }}*
13+
<template v-if="currentPlanDuration">
14+
/{{ currentPlanDuration }}
15+
</template>
1216
</template>
1317
</p>
1418
</div>
1519
<div class="sw-in-app-purchase-checkout-subscription-change__item">
1620
<p class="sw-in-app-purchase-checkout-subscription-change__plan-name">
1721
{{ $t('sw-in-app-purchase-checkout-subscription-change.new-plan') }}
18-
<span class="sw-in-app-purchase-checkout-subscription-change__note">
19-
({{ $t('sw-in-app-purchase-checkout-subscription-change.starting') }} {{ formattedStartingDate }})
20-
</span>
22+
<span class="sw-in-app-purchase-checkout-subscription-change__new-plan">({{ newPlanName }})</span>
2123
</p>
2224
<p class="sw-in-app-purchase-checkout-subscription-change__plan-price">
23-
{{ currencyFilter(cart.netPrice, 'EUR', 2) }}*/{{ $t('sw-in-app-purchase-price-box.duration.' + cartPosition.variant) }}
25+
{{ currencyFilter(cart.netPrice, 'EUR', 2) }}*
26+
<template v-if="newPlanDuration">
27+
/{{ newPlanDuration }}
28+
</template>
2429
</p>
2530
</div>
2631

@@ -31,11 +36,11 @@
3136
{{ $t('sw-in-app-purchase-checkout-subscription-change.due-today') }}
3237
</p>
3338
<p class="sw-in-app-purchase-checkout-subscription-change__due-day">
34-
{{ currencyFilter(cartPosition.proratedNetPrice, 'EUR', 2) }}*
39+
{{ proratedNetPrice }}*
3540
</p>
3641
</div>
3742

38-
<p class="sw-in-app-purchase-checkout-subscription-change__info-hint">
43+
<p v-if="infoHint" class="sw-in-app-purchase-checkout-subscription-change__info-hint">
3944
{{ infoHint }}
4045
</p>
4146

@@ -47,7 +52,7 @@
4752
>
4853
<strong>{{ $t('sw-in-app-purchase-checkout-subscription-change.attention') }}</strong>
4954
<span>
50-
{{ $t('sw-in-app-purchase-checkout-subscription-change.access-grant-hint', { plan: getCurrentPlanName }) }}
55+
{{ $t('sw-in-app-purchase-checkout-subscription-change.access-grant-hint', { plan: currentPlanName }) }}
5156
</span>
5257
</mt-banner>
5358
</div>

src/Resources/app/administration/src/module/sw-in-app-purchases/component/sw-in-app-purchase-checkout-subscription-change/sw-in-app-purchase-checkout-subscription-change.scss

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,29 @@
2121
line-height: var(--font-line-height-m);
2222
}
2323

24+
&__plan-name {
25+
overflow: hidden;
26+
white-space: nowrap;
27+
display: inline-flex;
28+
gap: 4px;
29+
align-items: center;
30+
flex: 1;
31+
}
32+
33+
&__current-plan, &__new-plan {
34+
color: var(--color-text-secondary-default);
35+
display: inline-block;
36+
overflow: hidden;
37+
text-overflow: ellipsis;
38+
min-width: 0;
39+
}
40+
2441
&__due-day {
2542
margin-top: 16px;
2643
font-weight: var(--font-weight-semibold);
2744
}
2845

29-
&__note, &__info-hin {
46+
&__note, &__info-hint {
3047
color: var(--color-text-secondary-default);
3148
}
3249

src/Resources/app/administration/src/module/sw-in-app-purchases/component/sw-in-app-purchase-checkout-subscription-change/sw-in-app-purchase-checkout-subscription-change.spec.js

Lines changed: 128 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,14 @@ const defaultCart = {
1818
variant: 'monthly',
1919
proratedNetPrice: 3.50,
2020
nextBookingDate: '2026-04-01',
21+
feature: {
22+
name: 'Pro Plan'
23+
},
2124
subscriptionChange: {
25+
type: 'upgrade',
2226
isIncludedInPluginLicense: false,
27+
currentFeatureVariant: 'monthly',
28+
currentNetPrice: 2.99,
2329
currentFeature: {
2430
name: 'Basic Plan',
2531
priceModels: [{
@@ -95,16 +101,112 @@ describe('sw-in-app-purchase-checkout-subscription-change', () => {
95101
expect(wrapper.vm.isIncludedInPluginLicense).toBe(true);
96102
});
97103

98-
it('should compute getCurrentPrice from matching variant price model', () => {
99-
expect(wrapper.vm.getCurrentPrice).toContain('2.99');
104+
it('should compute currentPrice from current net price', () => {
105+
expect(wrapper.vm.currentPrice).toContain('2.99');
106+
});
107+
108+
it('should compute currentPlanName from current feature', () => {
109+
expect(wrapper.vm.currentPlanName).toBe('Basic Plan');
110+
});
111+
112+
it('should compute currentPlanDuration from currentFeatureVariant', () => {
113+
expect(wrapper.vm.currentPlanDuration).toBe('sw-in-app-purchase-price-box.duration.monthly');
114+
});
115+
116+
it('should return null for currentPlanDuration when currentFeatureVariant is missing', async () => {
117+
wrapper = await createWrapper({
118+
cart: {
119+
...defaultCart,
120+
positions: [{
121+
...defaultCart.positions[0],
122+
subscriptionChange: {
123+
...defaultCart.positions[0].subscriptionChange,
124+
currentFeatureVariant: undefined
125+
}
126+
}]
127+
}
128+
});
129+
130+
expect(wrapper.vm.currentPlanDuration).toBeNull();
131+
});
132+
133+
it('should compute newPlanName from feature name', () => {
134+
expect(wrapper.vm.newPlanName).toBe('Pro Plan');
135+
});
136+
137+
it('should compute newPlanDuration from variant', () => {
138+
expect(wrapper.vm.newPlanDuration).toBe('sw-in-app-purchase-price-box.duration.monthly');
100139
});
101140

102-
it('should compute getCurrentPlanName from current feature', () => {
103-
expect(wrapper.vm.getCurrentPlanName).toBe('Basic Plan');
141+
it('should return null for newPlanDuration when variant is missing', async () => {
142+
wrapper = await createWrapper({
143+
cart: {
144+
...defaultCart,
145+
positions: [{
146+
...defaultCart.positions[0],
147+
variant: undefined
148+
}]
149+
}
150+
});
151+
152+
expect(wrapper.vm.newPlanDuration).toBeNull();
104153
});
105154

106-
it('should compute formattedStartingDate from nextBookingDate', () => {
107-
expect(wrapper.vm.formattedStartingDate).toBeTruthy();
155+
it('should compute proratedNetPrice for upgrade', () => {
156+
expect(wrapper.vm.proratedNetPrice).toContain('3.50');
157+
});
158+
159+
it('should return zero proratedNetPrice for downgrade', async () => {
160+
wrapper = await createWrapper({
161+
cart: {
162+
...defaultCart,
163+
positions: [{
164+
...defaultCart.positions[0],
165+
subscriptionChange: {
166+
...defaultCart.positions[0].subscriptionChange,
167+
type: 'downgrade'
168+
}
169+
}]
170+
}
171+
});
172+
173+
expect(wrapper.vm.proratedNetPrice).toContain('0.00');
174+
});
175+
176+
it('should compute isNextBookingDateInDifferentYear as false for same year', () => {
177+
expect(wrapper.vm.isNextBookingDateInDifferentYear).toBe(false);
178+
});
179+
180+
it('should compute isNextBookingDateInDifferentYear as true for different year', async () => {
181+
wrapper = await createWrapper({
182+
cart: {
183+
...defaultCart,
184+
positions: [{
185+
...defaultCart.positions[0],
186+
nextBookingDate: '2099-01-01'
187+
}]
188+
}
189+
});
190+
191+
expect(wrapper.vm.isNextBookingDateInDifferentYear).toBe(true);
192+
});
193+
194+
it('should include year in yearFormat when next booking date is in different year', async () => {
195+
wrapper = await createWrapper({
196+
cart: {
197+
...defaultCart,
198+
positions: [{
199+
...defaultCart.positions[0],
200+
nextBookingDate: '2099-01-01'
201+
}]
202+
}
203+
});
204+
205+
expect(wrapper.vm.yearFormat).toBe('numeric');
206+
});
207+
208+
it('should return undefined yearFormat when next booking date is in same year', () => {
209+
expect(wrapper.vm.yearFormat).toBeUndefined();
108210
});
109211

110212
it('should not render access-grant-hint banner when not included in plugin license', () => {
@@ -137,8 +239,26 @@ describe('sw-in-app-purchase-checkout-subscription-change', () => {
137239
expect(wrapper.find('.sw-in-app-purchase-checkout-subscription-change__divider').exists()).toBe(true);
138240
});
139241

140-
it('should render the info hint', () => {
242+
it('should render the info hint for upgrade', () => {
243+
expect(wrapper.find('.sw-in-app-purchase-checkout-subscription-change__info-hint').exists()).toBe(true);
244+
expect(wrapper.vm.infoHint).toBeTruthy();
245+
});
246+
247+
it('should render the info hint for downgrade', async () => {
248+
wrapper = await createWrapper({
249+
cart: {
250+
...defaultCart,
251+
positions: [{
252+
...defaultCart.positions[0],
253+
subscriptionChange: {
254+
...defaultCart.positions[0].subscriptionChange,
255+
type: 'downgrade'
256+
}
257+
}]
258+
}
259+
});
260+
141261
expect(wrapper.find('.sw-in-app-purchase-checkout-subscription-change__info-hint').exists()).toBe(true);
142-
expect(wrapper.find('.sw-in-app-purchase-checkout-subscription-change__info-hint').text()).toBeTruthy();
262+
expect(wrapper.vm.infoHint).toBeTruthy();
143263
});
144264
});

0 commit comments

Comments
 (0)