Skip to content

Commit 3d81bae

Browse files
committed
ECTO-2555 - Display hint when upgrading access granted IAP (#136)
1 parent 06201c4 commit 3d81bae

File tree

9 files changed

+284
-14
lines changed

9 files changed

+284
-14
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,19 @@ export default Shopware.Component.wrapComponentConfig({
5959
return this.cart.positions[0];
6060
},
6161

62+
isIncludedInPluginLicense() {
63+
return this.cartPosition?.subscriptionChange?.isIncludedInPluginLicense ?? false;
64+
},
65+
6266
getCurrentPrice() {
6367
const price = this.cartPosition?.subscriptionChange?.currentFeature?.priceModels
6468
?.find((priceModel) => priceModel.variant === this.cartPosition.variant)?.price;
6569

6670
return String(this.currencyFilter(price, 'EUR', 2));
71+
},
72+
73+
getCurrentPlanName() {
74+
return this.cartPosition?.subscriptionChange?.currentFeature?.name;
6775
}
6876
}
6977
});

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: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
{{ $t('sw-in-app-purchase-checkout-subscription-change.current-plan') }}
55
</p>
66
<p class="sw-in-app-purchase-checkout-subscription-change__plan-price">
7-
{{ getCurrentPrice }}*/{{ $t('sw-in-app-purchase-price-box.duration.' + cartPosition.variant) }}
7+
<template v-if="isIncludedInPluginLicense">
8+
{{ $t('sw-in-app-purchase-checkout-subscription-change.free') }}
9+
</template>
10+
<template v-else>
11+
{{ getCurrentPrice }}*/{{ $t('sw-in-app-purchase-price-box.duration.' + cartPosition.variant) }}
12+
</template>
813
</p>
914
</div>
1015
<div class="sw-in-app-purchase-checkout-subscription-change__item">
@@ -31,6 +36,18 @@
3136
</div>
3237

3338
<p class="sw-in-app-purchase-checkout-subscription-change__info-hint">
34-
{{ infoHint }}
35-
</p>
39+
{{ infoHint }}
40+
</p>
41+
42+
<mt-banner
43+
v-if="isIncludedInPluginLicense"
44+
variant="attention"
45+
:show-icon="true"
46+
class="sw-in-app-purchase-checkout-subscription-change__access-grant-hint"
47+
>
48+
<strong>{{ $t('sw-in-app-purchase-checkout-subscription-change.attention') }}</strong>
49+
<span>
50+
{{ $t('sw-in-app-purchase-checkout-subscription-change.access-grant-hint', { plan: getCurrentPlanName }) }}
51+
</span>
52+
</mt-banner>
3653
</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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,25 @@
3333
&__divider {
3434
border-bottom: 1px solid var(--color-shopware-brand-900);
3535
}
36+
37+
&__access-grant-hint.mt-banner {
38+
width: 100%;
39+
padding: 16px;
40+
margin: 0;
41+
display: flex;
42+
gap: 16px;
43+
font-size: var(--font-size-xs);
44+
45+
.mt-banner__icon {
46+
position: unset;
47+
}
48+
49+
.mt-banner__body {
50+
padding: 0;
51+
}
52+
53+
strong {
54+
font-weight: var(--font-weight-semibold);
55+
}
56+
}
3657
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { mount } from '@vue/test-utils';
2+
3+
Shopware.Component.register(
4+
'sw-in-app-purchase-checkout-subscription-change',
5+
() => import('SwagExtensionStore/module/sw-in-app-purchases/component/sw-in-app-purchase-checkout-subscription-change')
6+
);
7+
8+
jest.mock('SwagExtensionStore/module/sw-in-app-purchases/types', () => ({
9+
InAppPurchase: jest.fn()
10+
}));
11+
12+
const defaultCart = {
13+
netPrice: 5.99,
14+
grossPrice: 7.13,
15+
taxPrice: 7.13,
16+
taxValue: 19,
17+
positions: [{
18+
variant: 'monthly',
19+
proratedNetPrice: 3.50,
20+
nextBookingDate: '2026-04-01',
21+
subscriptionChange: {
22+
isIncludedInPluginLicense: false,
23+
currentFeature: {
24+
name: 'Basic Plan',
25+
priceModels: [{
26+
type: 'rent',
27+
price: 2.99,
28+
duration: 1,
29+
variant: 'monthly'
30+
}]
31+
}
32+
}
33+
}]
34+
};
35+
36+
async function createWrapper(props = {}) {
37+
return mount(await Shopware.Component.build('sw-in-app-purchase-checkout-subscription-change'), {
38+
props: {
39+
purchase: {
40+
priceModels: [{
41+
type: 'rent',
42+
price: 5.99,
43+
duration: 1,
44+
variant: 'monthly'
45+
}]
46+
},
47+
cart: defaultCart,
48+
...props
49+
},
50+
global: {
51+
stubs: {
52+
'mt-banner': true
53+
}
54+
}
55+
});
56+
}
57+
58+
describe('sw-in-app-purchase-checkout-subscription-change', () => {
59+
let wrapper;
60+
61+
beforeEach(async () => {
62+
wrapper = await createWrapper();
63+
});
64+
65+
it('should be a Vue.js component', () => {
66+
expect(wrapper.vm).toBeTruthy();
67+
});
68+
69+
it('should render correctly', () => {
70+
expect(wrapper.exists()).toBe(true);
71+
});
72+
73+
it('should compute cartPosition from first cart position', () => {
74+
expect(wrapper.vm.cartPosition).toEqual(defaultCart.positions[0]);
75+
});
76+
77+
it('should compute isIncludedInPluginLicense as false by default', () => {
78+
expect(wrapper.vm.isIncludedInPluginLicense).toBe(false);
79+
});
80+
81+
it('should compute isIncludedInPluginLicense as true when set', async () => {
82+
wrapper = await createWrapper({
83+
cart: {
84+
...defaultCart,
85+
positions: [{
86+
...defaultCart.positions[0],
87+
subscriptionChange: {
88+
...defaultCart.positions[0].subscriptionChange,
89+
isIncludedInPluginLicense: true
90+
}
91+
}]
92+
}
93+
});
94+
95+
expect(wrapper.vm.isIncludedInPluginLicense).toBe(true);
96+
});
97+
98+
it('should compute getCurrentPrice from matching variant price model', () => {
99+
expect(wrapper.vm.getCurrentPrice).toContain('2.99');
100+
});
101+
102+
it('should compute getCurrentPlanName from current feature', () => {
103+
expect(wrapper.vm.getCurrentPlanName).toBe('Basic Plan');
104+
});
105+
106+
it('should compute formattedStartingDate from nextBookingDate', () => {
107+
expect(wrapper.vm.formattedStartingDate).toBeTruthy();
108+
});
109+
110+
it('should not render access-grant-hint banner when not included in plugin license', () => {
111+
expect(wrapper.find('.sw-in-app-purchase-checkout-subscription-change__access-grant-hint').exists()).toBe(false);
112+
});
113+
114+
it('should render access-grant-hint banner when included in plugin license', async () => {
115+
wrapper = await createWrapper({
116+
cart: {
117+
...defaultCart,
118+
positions: [{
119+
...defaultCart.positions[0],
120+
subscriptionChange: {
121+
...defaultCart.positions[0].subscriptionChange,
122+
isIncludedInPluginLicense: true
123+
}
124+
}]
125+
}
126+
});
127+
128+
expect(wrapper.find('.sw-in-app-purchase-checkout-subscription-change__access-grant-hint').exists()).toBe(true);
129+
});
130+
131+
it('should render current plan and new plan sections', () => {
132+
const items = wrapper.findAll('.sw-in-app-purchase-checkout-subscription-change__item');
133+
expect(items).toHaveLength(3);
134+
});
135+
136+
it('should render the divider', () => {
137+
expect(wrapper.find('.sw-in-app-purchase-checkout-subscription-change__divider').exists()).toBe(true);
138+
});
139+
140+
it('should render the info hint', () => {
141+
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();
143+
});
144+
});

src/Resources/app/administration/src/module/sw-in-app-purchases/snippet/de-DE.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@
5656
"new-plan": "Neuer Plan",
5757
"starting": "Beginn",
5858
"due-today": "Heute fällig",
59-
"info-lint": "Basierend auf dem heutigen Datum {today} beträgt Ihre anteilige Upgrade-Gebühr {price}. Ihre reguläre {variant} Gebühr von {fee} beginnt am {start}."
59+
"info-lint": "Basierend auf dem heutigen Datum {today} beträgt Ihre anteilige Upgrade-Gebühr {price}. Ihre reguläre {variant} Gebühr von {fee} beginnt am {start}.",
60+
"attention": "Bitte beachte:",
61+
"access-grant-hint": "du wirst dauerhaft den kostenlosen Zugriff auf deinen aktuellen Plan \"{plan}\" verlieren.",
62+
"free": "kostenlos"
6063
}
6164
}

src/Resources/app/administration/src/module/sw-in-app-purchases/snippet/en-GB.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@
5656
"new-plan": "New plan",
5757
"starting": "Starting",
5858
"due-today": "Due today",
59-
"info-lint": "Based on today’s date {today}, your prorated upgrade charge is {price}. Your regular {variant} charge of {fee} will start on {start}."
59+
"info-lint": "Based on today’s date {today}, your prorated upgrade charge is {price}. Your regular {variant} charge of {fee} will start on {start}.",
60+
"attention": "Please note:",
61+
"access-grant-hint": "you'll permanently lose free access to your current plan '{plan}'.",
62+
"free": "free"
6063
}
6164
}

src/Resources/app/administration/src/module/sw-in-app-purchases/types/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,19 @@ export type InAppPurchase = {
2121
preselectedVariant: string;
2222
};
2323

24+
export type InAppPendingDowngrade = {
25+
feature: InAppPurchase;
26+
netPrice: number;
27+
};
28+
2429
export type InAppSubscriptionChange = {
2530
id: string;
2631
type: 'upgrade' | 'downgrade';
2732
currentNetPrice: number;
2833
currentFeatureVariant: string;
2934
currentFeature: InAppPurchase;
30-
pendingDowngrade: string;
35+
pendingDowngrade: null | InAppPendingDowngrade;
36+
isIncludedInPluginLicense: boolean;
3137
};
3238

3339
export type InAppPurchaseCartPosition = {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace SwagExtensionStore\Struct;
4+
5+
use Shopware\Core\Framework\Log\Package;
6+
use Shopware\Core\Framework\Struct\Struct;
7+
8+
/**
9+
* @codeCoverageIgnore
10+
*
11+
* @phpstan-import-type InAppPurchase from InAppPurchaseStruct
12+
*
13+
* @phpstan-type InAppPurchasePendingDowngrade array{feature: InAppPurchase, netPrice: float}
14+
*/
15+
#[Package('checkout')]
16+
class InAppPurchasePendingDowngradeStruct extends Struct
17+
{
18+
private function __construct(
19+
protected InAppPurchaseStruct $feature,
20+
protected float $netPrice = 0.0,
21+
) {
22+
}
23+
24+
/**
25+
* @param InAppPurchasePendingDowngrade $data
26+
*/
27+
public static function fromArray(array $data): self
28+
{
29+
$feature = InAppPurchaseStruct::fromArray($data['feature']);
30+
31+
return (new self($feature))->assign($data);
32+
}
33+
34+
public function getFeature(): InAppPurchaseStruct
35+
{
36+
return $this->feature;
37+
}
38+
39+
public function setFeature(InAppPurchaseStruct $feature): void
40+
{
41+
$this->feature = $feature;
42+
}
43+
44+
public function getNetPrice(): float
45+
{
46+
return $this->netPrice;
47+
}
48+
49+
public function setNetPrice(float $netPrice): void
50+
{
51+
$this->netPrice = $netPrice;
52+
}
53+
}

src/Struct/InAppPurchaseSubscriptionChangeStruct.php

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,20 @@
99
* @codeCoverageIgnore
1010
*
1111
* @phpstan-import-type InAppPurchase from InAppPurchaseStruct
12+
* @phpstan-import-type InAppPurchasePendingDowngrade from InAppPurchasePendingDowngradeStruct
1213
*
13-
* @phpstan-type InAppPurchaseSubscriptionChange array{currentFeature: InAppPurchase, type: string, currentFeatureVariant: string, currentNetPrice: string, pendingDowngrade: string}
14+
* @phpstan-type InAppPurchaseSubscriptionChange array{currentFeature: InAppPurchase, type: string, currentFeatureVariant: string, currentNetPrice: float, pendingDowngrade: InAppPurchasePendingDowngrade|null, isIncludedInPluginLicense: bool}
1415
*/
1516
#[Package('checkout')]
1617
class InAppPurchaseSubscriptionChangeStruct extends Struct
1718
{
1819
private function __construct(
1920
protected InAppPurchaseStruct $currentFeature,
21+
protected ?InAppPurchasePendingDowngradeStruct $pendingDowngrade = null,
2022
protected string $type = '',
2123
protected string $currentFeatureVariant = '',
22-
protected string $currentNetPrice = '',
23-
protected string $pendingDowngrade = '',
24+
protected float $currentNetPrice = 0.0,
25+
protected bool $isIncludedInPluginLicense = false,
2426
) {
2527
}
2628

@@ -29,7 +31,10 @@ private function __construct(
2931
*/
3032
public static function fromArray(array $data): self
3133
{
32-
return (new self(InAppPurchaseStruct::fromArray($data['currentFeature'])))->assign($data);
34+
$currentFeature = InAppPurchaseStruct::fromArray($data['currentFeature']);
35+
$pendingDowngrade = isset($data['pendingDowngrade']) ? InAppPurchasePendingDowngradeStruct::fromArray($data['pendingDowngrade']) : null;
36+
37+
return (new self($currentFeature, $pendingDowngrade))->assign($data);
3338
}
3439

3540
/**
@@ -73,23 +78,33 @@ public function setCurrentFeatureVariant(string $currentFeatureVariant): void
7378
$this->currentFeatureVariant = $currentFeatureVariant;
7479
}
7580

76-
public function getCurrentNetPrice(): string
81+
public function getCurrentNetPrice(): float
7782
{
7883
return $this->currentNetPrice;
7984
}
8085

81-
public function setCurrentNetPrice(string $currentNetPrice): void
86+
public function setCurrentNetPrice(float $currentNetPrice): void
8287
{
8388
$this->currentNetPrice = $currentNetPrice;
8489
}
8590

86-
public function getPendingDowngrade(): string
91+
public function getPendingDowngrade(): ?InAppPurchasePendingDowngradeStruct
8792
{
8893
return $this->pendingDowngrade;
8994
}
9095

91-
public function setPendingDowngrade(string $pendingDowngrade): void
96+
public function setPendingDowngrade(?InAppPurchasePendingDowngradeStruct $pendingDowngrade): void
9297
{
9398
$this->pendingDowngrade = $pendingDowngrade;
9499
}
100+
101+
public function isIncludedInPluginLicense(): bool
102+
{
103+
return $this->isIncludedInPluginLicense;
104+
}
105+
106+
public function setIncludedInPluginLicense(bool $isIncludedInPluginLicense): void
107+
{
108+
$this->isIncludedInPluginLicense = $isIncludedInPluginLicense;
109+
}
95110
}

0 commit comments

Comments
 (0)