Skip to content

Commit f5dc076

Browse files
authored
feat: iap upgrade and downgrade (#110)
1 parent 0850d5e commit f5dc076

28 files changed

+490
-83
lines changed

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/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type * as IAP from 'SwagExtensionStore/module/sw-in-app-purchases/types';
22
import template from './sw-in-app-purchase-checkout-button.html.twig';
3+
import './sw-in-app-purchase-checkout-button.scss';
34

45
/**
56
* @private
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.sw-in-app-purchase-checkout-button.mt-button {
2+
width: 100%;
3+
}

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

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,31 +27,52 @@ export default Shopware.Component.wrapComponentConfig({
2727
type: String,
2828
required: true,
2929
},
30+
cart: {
31+
type: Object as PropType<IAP.InAppPurchaseCart>,
32+
required: true,
33+
},
34+
variant: {
35+
type: String,
36+
required: true,
37+
},
3038
},
3139

3240
data(): {
3341
showConditionsModal: boolean;
34-
priceModel: IAP.InAppPurchasePriceModel;
3542
} {
3643
return {
3744
showConditionsModal: false,
38-
priceModel: this.purchase.priceModels[0],
3945
};
4046
},
4147

42-
created() {
43-
this.setPriceModel();
48+
watch: {
49+
priceModel: {
50+
immediate: true,
51+
handler() {
52+
this.onGtcAcceptedChange(this.priceModel.conditionsType === null);
53+
},
54+
},
4455
},
4556

4657
computed: {
47-
purchaseOptions(): Array<{ value: IAP.InAppPurchasePriceModel; name: string }> {
48-
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 } => {
4960
return {
50-
value: priceModel,
61+
value: priceModel.variant,
5162
name: `€${priceModel.price}* /${this.$t(`sw-in-app-purchase-price-box.duration.${priceModel.variant}`)}`,
5263
};
5364
});
5465
},
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);
75+
},
5576
},
5677

5778
methods: {
@@ -71,13 +92,10 @@ export default Shopware.Component.wrapComponentConfig({
7192
this.$emit('update:gtc-accepted', value);
7293
},
7394

74-
setPriceModel(priceModel?: IAP.InAppPurchasePriceModel) {
75-
if (!priceModel) {
76-
priceModel = this.purchase.priceModels[0];
95+
updateVariant(variant : string) {
96+
if (this.variant !== variant) {
97+
this.$emit('update:variant', variant);
7798
}
78-
this.priceModel = priceModel;
79-
this.onGtcAcceptedChange(priceModel.conditionsType === null);
80-
this.$emit('update:variant', priceModel.variant);
8199
},
82100
},
83101
});

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-overview.spec.js

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

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

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

98-
// now we call it with a different price model, to see if it updates and emits accordingly
99-
wrapper.vm.setPriceModel(priceModel);
100-
expect(wrapper.vm.priceModel).toStrictEqual(priceModel);
101-
expect(wrapper.emitted('update:gtc-accepted')).toBeTruthy();
102-
expect(wrapper.emitted('update:gtc-accepted')[1]).toEqual([true]);
103-
expect(wrapper.emitted('update:variant')).toBeTruthy();
104-
expect(wrapper.emitted('update:variant')[1]).toStrictEqual(['yearly']);
98+
it('should render subscription change card', async () => {
99+
await wrapper.setProps({
100+
cart: {
101+
positions: [{
102+
variant: 1,
103+
subscriptionChange: 'upgrade',
104+
}],
105+
},
106+
});
107+
108+
expect(wrapper.find('sw-in-app-purchase-checkout-subscription-change-stub')).toBeTruthy();
105109
});
106110
});
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.Store.get('session').currentLocale ??
22+
Shopware.Store.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: var(--scale-size-16);
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: var(--scale-size-24);
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: var(--scale-size-16);
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: var(--scale-size-16);
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)