Skip to content

Commit 06f95c6

Browse files
authored
feat: iap upgrade and downgrade (#117)
* feat: iap upgrade and downgrade (#110) * fix: pipeline * styling: fix spacing * chore: increase composer version
1 parent 932f5ab commit 06f95c6

29 files changed

+502
-93
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "swag/swag-extension-store",
3-
"version": "3.1.7",
3+
"version": "3.2.0",
44
"description": "SWAG Extension Store",
55
"type": "shopware-platform-plugin",
66
"license": "MIT",

src/Controller/InAppPurchasesController.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
use SwagExtensionStore\Exception\ExtensionStoreException;
2020
use SwagExtensionStore\Services\InAppPurchasesService;
2121
use SwagExtensionStore\Struct\InAppPurchaseCartPositionCollection;
22-
use SwagExtensionStore\Struct\InAppPurchaseStruct;
2322
use Symfony\Component\HttpFoundation\JsonResponse;
2423
use Symfony\Component\HttpFoundation\Response;
2524
use Symfony\Component\Routing\Attribute\Route;
@@ -140,12 +139,9 @@ public function refreshInAppPurchases(Context $context): Response
140139
#[Route('/api/_action/in-app-purchases/{technicalName}/{inAppPurchase}', name: 'api.in-app-purchases.in-app-purchase', methods: ['GET'])]
141140
public function getInAppPurchase(string $technicalName, string $inAppPurchase, Context $context): Response
142141
{
143-
$inAppPurchaseCollection = $this->inAppPurchasesService->listPurchases($technicalName, $context);
144-
$iap = $inAppPurchaseCollection->filter(
145-
fn (InAppPurchaseStruct $availableInAppPurchases) => $availableInAppPurchases->getIdentifier() === $inAppPurchase
146-
)->first();
142+
$purchase = $this->inAppPurchasesService->getInAppPurchase($technicalName, $inAppPurchase, $context);
147143

148-
return new JsonResponse($iap);
144+
return new JsonResponse($purchase);
149145
}
150146

151147
private function getAppByName(string $appName, Context $context): ?AppEntity

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
.sw-in-app-purchase-checkout {
2+
.mt-button {
3+
width: 100%;
4+
}
5+
26
.sw-modal {
37
&__footer {
48
grid-template-columns: 1fr;

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

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import './sw-in-app-purchase-checkout-overview.scss';
88
export default Shopware.Component.wrapComponentConfig({
99
template,
1010

11+
emits: ['update:tos-accepted', 'update:gtc-accepted', 'update:variant'],
12+
1113
props: {
1214
purchase: {
1315
type: Object as PropType<IAP.InAppPurchase>,
@@ -24,31 +26,52 @@ export default Shopware.Component.wrapComponentConfig({
2426
producer: {
2527
type: String,
2628
required: true
29+
},
30+
cart: {
31+
type: Object as PropType<IAP.InAppPurchaseCart>,
32+
required: true
33+
},
34+
variant: {
35+
type: String,
36+
required: true
2737
}
2838
},
2939

3040
data(): {
3141
showConditionsModal: boolean;
32-
priceModel: IAP.InAppPurchasePriceModel;
3342
} {
3443
return {
35-
showConditionsModal: false,
36-
priceModel: this.purchase.priceModels[0]
44+
showConditionsModal: false
3745
};
3846
},
3947

40-
created() {
41-
this.setPriceModel();
48+
watch: {
49+
priceModel: {
50+
immediate: true,
51+
handler() {
52+
this.onGtcAcceptedChange(this.priceModel.conditionsType === null);
53+
}
54+
}
4255
},
4356

4457
computed: {
45-
purchaseOptions(): Array<{ value: IAP.InAppPurchasePriceModel; name: string }> {
46-
return this.purchase.priceModels.map((priceModel): { value: IAP.InAppPurchasePriceModel; name: string } => {
58+
purchaseOptions(): Array<{ value: string; name: string }> {
59+
return this.purchase.priceModels.map((priceModel): { value: string; name: string } => {
4760
return {
48-
value: priceModel,
61+
value: priceModel.variant,
4962
name: `€${priceModel.price}* /${this.$t(`sw-in-app-purchase-price-box.duration.${priceModel.variant}`)}`
5063
};
5164
});
65+
},
66+
67+
priceModel(): IAP.InAppPurchasePriceModel {
68+
return this.purchase.priceModels.find(
69+
(pm: IAP.InAppPurchasePriceModel) => this.cart.positions[0].variant === pm.variant
70+
) || this.purchase.priceModels[0];
71+
},
72+
73+
subscriptionChange() {
74+
return this.cart.positions.find(position => position.subscriptionChange !== null);
5275
}
5376
},
5477

@@ -69,13 +92,10 @@ export default Shopware.Component.wrapComponentConfig({
6992
this.$emit('update:gtc-accepted', value);
7093
},
7194

72-
setPriceModel(priceModel?: IAP.InAppPurchasePriceModel) {
73-
if (!priceModel) {
74-
priceModel = this.purchase.priceModels[0];
95+
updateVariant(variant : string) {
96+
if (this.variant !== variant) {
97+
this.$emit('update:variant', variant);
7598
}
76-
this.priceModel = priceModel;
77-
this.onGtcAcceptedChange(priceModel.conditionsType === null);
78-
this.$emit('update:variant', priceModel.variant);
7999
}
80100
}
81101
});

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,24 @@
99
</p>
1010
</div>
1111

12+
<sw-in-app-purchase-checkout-subscription-change
13+
v-if="subscriptionChange"
14+
:purchase="purchase"
15+
:cart="cart"
16+
/>
17+
1218
<sw-in-app-purchase-price-box
13-
v-if="purchase.priceModels.length === 1"
19+
v-else-if="purchase.priceModels.length === 1"
1420
:price-model="priceModel"
1521
/>
1622

1723
<sw-radio-field
1824
v-else
1925
block
26+
:value="variant"
2027
:options="purchaseOptions"
2128
class="sw-in-app-purchase-checkout-purchase__feature__choices"
22-
@update:value="setPriceModel"
29+
@update:value="updateVariant"
2330
/>
2431

2532
<div class="sw-in-app-purchase-checkout-purchase__subtext">

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
font-weight: var(--font-weight-regular);
77
font-size: var(--font-size-xs);
88

9+
.sw-field--checkbox {
10+
margin-bottom: 0;
11+
}
12+
913
.sw-gtc-checkbox {
1014
.sw-field__label {
1115
color: var(--color-text-primary-default);
1216
}
13-
14-
.sw-field--checkbox {
15-
margin-bottom: 0;
16-
}
1717
}
1818

1919
&__feature {
@@ -40,7 +40,7 @@
4040
padding: 25px 19px;
4141

4242
.sw-field__radio-input {
43-
margin-top: var(--scale-size-4);
43+
margin-top: 4px;
4444
}
4545

4646
.sw-field__radio-option-label {

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

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,23 @@ async function createWrapper() {
2424
},
2525
tosAccepted: false,
2626
gtcAccepted: false,
27-
producer: 'shopware'
27+
producer: 'shopware',
28+
variant: 'monthly',
29+
cart: {
30+
netPrice: 1,
31+
grossPrice: 2.99,
32+
taxPrice: 2.99,
33+
taxValue: 4,
34+
positions: [{
35+
variant: 1,
36+
subscriptionChange: null
37+
}]
38+
}
2839
},
2940
global: {
3041
stubs: {
3142
'sw-in-app-purchase-price-box': true,
43+
'sw-in-app-purchase-checkout-subscription-change': true,
3244
'sw-gtc-checkbox': true,
3345
'sw-radio-field': true,
3446
'sw-button': true
@@ -79,29 +91,21 @@ describe('sw-in-app-purchase-checkout-overview', () => {
7991
expect(wrapper.vm.showConditionsModal).toBe(false);
8092
});
8193

82-
it('should set the priceModel and emit update:variant when setPriceModel is called', async () => {
83-
// when the component is created, the first price model is set emitting this data
84-
// setPriceModel is called during the component creation, therefor we don't need to explicitly test it
85-
expect(wrapper.vm.priceModel).toStrictEqual(wrapper.vm.purchase.priceModels[0]);
86-
expect(wrapper.emitted('update:gtc-accepted')).toBeTruthy();
87-
expect(wrapper.emitted('update:gtc-accepted')[0]).toEqual([true]);
88-
expect(wrapper.emitted('update:variant')).toBeTruthy();
89-
expect(wrapper.emitted('update:variant')[0]).toStrictEqual(['monthly']);
9094

91-
const priceModel = {
92-
type: 'rent',
93-
price: 10.99,
94-
duration: 12,
95-
variant: 'yearly',
96-
conditionsType: null
97-
};
95+
it('should render not subscription change card', async () => {
96+
expect(wrapper.find('sw-in-app-purchase-checkout-subscription-change-stub').exists()).toBeFalsy();
97+
});
9898

99-
// now we call it with a different price model, to see if it updates and emits accordingly
100-
wrapper.vm.setPriceModel(priceModel);
101-
expect(wrapper.vm.priceModel).toStrictEqual(priceModel);
102-
expect(wrapper.emitted('update:gtc-accepted')).toBeTruthy();
103-
expect(wrapper.emitted('update:gtc-accepted')[1]).toEqual([true]);
104-
expect(wrapper.emitted('update:variant')).toBeTruthy();
105-
expect(wrapper.emitted('update:variant')[1]).toStrictEqual(['yearly']);
99+
it('should render subscription change card', async () => {
100+
await wrapper.setProps({
101+
cart: {
102+
positions: [{
103+
variant: 1,
104+
subscriptionChange: 'upgrade'
105+
}]
106+
}
107+
});
108+
109+
expect(wrapper.find('sw-in-app-purchase-checkout-subscription-change-stub')).toBeTruthy();
106110
});
107111
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type * as IAP from 'SwagExtensionStore/module/sw-in-app-purchases/types';
2+
import template from './sw-in-app-purchase-checkout-subscription-change.html.twig';
3+
import './sw-in-app-purchase-checkout-subscription-change.scss';
4+
5+
export default Shopware.Component.wrapComponentConfig({
6+
template,
7+
8+
props: {
9+
purchase: {
10+
type: Object as PropType<IAP.InAppPurchase>,
11+
required: true
12+
},
13+
cart: {
14+
type: Object as PropType<IAP.InAppPurchaseCart>,
15+
required: true
16+
}
17+
},
18+
19+
computed: {
20+
locale() {
21+
const local = String(Shopware.State.get('session').currentLocale ??
22+
Shopware.State.get('context').app?.fallbackLocale ?? 'en-GB');
23+
24+
return new Intl.Locale(local);
25+
},
26+
27+
currencyFilter() {
28+
return Shopware.Filter.getByName('currency');
29+
},
30+
31+
formattedStartingDate(): string {
32+
const date = new Date(this.cartPosition.nextBookingDate ?? '');
33+
return date.toLocaleDateString(this.locale, { month: 'numeric', day: 'numeric' });
34+
},
35+
36+
infoHint(): string {
37+
const today = new Date().toLocaleDateString(this.locale, {
38+
month: 'long',
39+
day: 'numeric'
40+
});
41+
42+
const nextBookingDate = this.cartPosition?.nextBookingDate
43+
? new Date(this.cartPosition.nextBookingDate).toLocaleDateString(this.locale, {
44+
month: 'long',
45+
day: 'numeric'
46+
})
47+
: '';
48+
49+
return this.$t('sw-in-app-purchase-checkout-subscription-change.info-lint', {
50+
today,
51+
price: this.currencyFilter(this.cartPosition?.proratedNetPrice, 'EUR', 2),
52+
variant: this.cartPosition?.variant ?? '',
53+
fee: this.currencyFilter(this.cart.netPrice, 'EUR', 2),
54+
start: nextBookingDate
55+
});
56+
},
57+
58+
cartPosition() {
59+
return this.cart.positions[0];
60+
},
61+
62+
getCurrentPrice() {
63+
const price = this.cartPosition?.subscriptionChange?.currentFeature?.priceModels
64+
?.find((priceModel) => priceModel.variant === this.cartPosition.variant)?.price;
65+
66+
return String(this.currencyFilter(price, 'EUR', 2));
67+
}
68+
}
69+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<div class="sw-in-app-purchase-checkout-subscription-change">
2+
<div class="sw-in-app-purchase-checkout-subscription-change__item">
3+
<p class="sw-in-app-purchase-checkout-subscription-change__plan-name">
4+
{{ $t('sw-in-app-purchase-checkout-subscription-change.current-plan') }}
5+
</p>
6+
<p class="sw-in-app-purchase-checkout-subscription-change__plan-price">
7+
{{ getCurrentPrice }}*/{{ $t('sw-in-app-purchase-price-box.duration.' + cartPosition.variant) }}
8+
</p>
9+
</div>
10+
<div class="sw-in-app-purchase-checkout-subscription-change__item">
11+
<p class="sw-in-app-purchase-checkout-subscription-change__plan-name">
12+
{{ $t('sw-in-app-purchase-checkout-subscription-change.new-plan') }}
13+
<span class="sw-in-app-purchase-checkout-subscription-change__note">
14+
({{ $t('sw-in-app-purchase-checkout-subscription-change.starting') }} {{ formattedStartingDate }})
15+
</span>
16+
</p>
17+
<p class="sw-in-app-purchase-checkout-subscription-change__plan-price">
18+
{{ currencyFilter(cart.netPrice, 'EUR', 2) }}*/{{ $t('sw-in-app-purchase-price-box.duration.' + cartPosition.variant) }}
19+
</p>
20+
</div>
21+
22+
<div class="sw-in-app-purchase-checkout-subscription-change__divider" />
23+
24+
<div class="sw-in-app-purchase-checkout-subscription-change__item">
25+
<p class="sw-in-app-purchase-checkout-subscription-change__due-day">
26+
{{ $t('sw-in-app-purchase-checkout-subscription-change.due-today') }}
27+
</p>
28+
<p class="sw-in-app-purchase-checkout-subscription-change__due-day">
29+
{{ currencyFilter(cartPosition.proratedNetPrice, 'EUR', 2) }}*
30+
</p>
31+
</div>
32+
33+
<p class="sw-in-app-purchase-checkout-subscription-change__info-hint">
34+
{{ infoHint }}
35+
</p>
36+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.sw-in-app-purchase-checkout-subscription-change {
2+
padding: 16px;
3+
background-color: var(--color-background-brand-default);
4+
border: 1px solid var(--color-shopware-brand-900);
5+
border-radius: var(--border-radius-default);
6+
display: flex;
7+
flex-direction: column;
8+
gap: 24px;
9+
font-size: var(--font-size-xs);
10+
line-height: var(--font-line-height-xs);
11+
12+
&__item {
13+
display: flex;
14+
justify-content: space-between;
15+
align-items: center;
16+
gap: 16px;
17+
}
18+
19+
&__plan-price, &__due-day {
20+
font-size: var(--font-size-m);
21+
line-height: var(--font-line-height-m);
22+
}
23+
24+
&__due-day {
25+
margin-top: 16px;
26+
font-weight: var(--font-weight-semibold);
27+
}
28+
29+
&__note, &__info-hin {
30+
color: var(--color-text-secondary-default);
31+
}
32+
33+
&__divider {
34+
border-bottom: 1px solid var(--color-shopware-brand-900);
35+
}
36+
}

0 commit comments

Comments
 (0)