Skip to content

Commit dc28f0f

Browse files
Add the required tier and fix the missing notice when a feature is already unlocked
1 parent 4e5b1d8 commit dc28f0f

8 files changed

Lines changed: 243 additions & 22 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{
2+
"reputationLog": {
3+
"id": 11,
4+
"tokens": 350,
5+
"localeKey": "sku.reputationlog",
6+
"skuEntry": {"skus": [{"skuId": "1380621847191883786"}, {"skuId": "1360377703743623261"}]},
7+
"defaultSize": 2
8+
},
9+
"analyzerLog": {
10+
"id": 2,
11+
"tokens": 350,
12+
"localeKey": "sku.analyzerlog",
13+
"skuEntry": {"skus": [{"skuId": "1380621847191883786"}, {"skuId": "1360377703743623261"}]},
14+
"extendedLogHours": 96
15+
},
16+
"reputationCategories": {
17+
"id": 10,
18+
"tokens": 175,
19+
"localeKey": "sku.reputationcategories",
20+
"skuEntry": {"skus": [{"skuId": "1380621847191883786"}, {"skuId": "1360377703743623261"}]},
21+
"defaultCategories": 2
22+
},
23+
"reputationChannel": {
24+
"id": 10,
25+
"tokens": 175,
26+
"localeKey": "sku.reputationchannel",
27+
"skuEntry": {"skus": [{"skuId": "1380621847191883786"}, {"skuId": "1360377703743623261"}]},
28+
"defaultChannel": 8
29+
},
30+
"channelBlacklist": {
31+
"id": 0,
32+
"tokens": 350,
33+
"localeKey": "sku.channelblacklist",
34+
"skuEntry": {"skus": [{"skuId": "1380621847191883786"}, {"skuId": "1360377703743623261"}]}
35+
},
36+
"localeOverrides": {
37+
"id": 7,
38+
"tokens": 350,
39+
"localeKey": "sku.localeoverrides",
40+
"skuEntry": {"skus": [{"skuId": "1360377703743623261"}]}
41+
},
42+
"autopost": {
43+
"id": 3,
44+
"tokens": 350,
45+
"localeKey": "sku.autopost",
46+
"skuEntry": {"skus": [{"skuId": "1380621847191883786"}, {"skuId": "1360377703743623261"}]}
47+
},
48+
"advancedRankings": {
49+
"id": 1,
50+
"tokens": 350,
51+
"localeKey": "sku.advancedrankings",
52+
"skuEntry": {"skus": [{"skuId": "1380621847191883786"}, {"skuId": "1360377703743623261"}]}
53+
},
54+
"detailedProfile": {
55+
"id": 5,
56+
"tokens": 350,
57+
"localeKey": "sku.detailedprofile",
58+
"skuEntry": {"skus": [{"skuId": "1380621847191883786"}, {"skuId": "1360377703743623261"}]}
59+
},
60+
"logChannel": {
61+
"id": 8,
62+
"tokens": 350,
63+
"localeKey": "sku.logchannel",
64+
"skuEntry": {"skus": [{"skuId": "1380621847191883786"}, {"skuId": "1360377703743623261"}]}
65+
},
66+
"additionalEmojis": {
67+
"id": 0,
68+
"tokens": 350,
69+
"localeKey": "sku.additionalemojis",
70+
"skuEntry": {"skus": [{"skuId": "1360377703743623261"}]}
71+
},
72+
"profile": {
73+
"id": 9,
74+
"tokens": 350,
75+
"localeKey": "sku.profile",
76+
"skuEntry": {"skus": [{"skuId": "1360377703743623261"}]}
77+
},
78+
"integrationBypass": {
79+
"id": 6,
80+
"tokens": 350,
81+
"localeKey": "sku.integrationbypass",
82+
"skuEntry": {"skus": [{"skuId": "1380621847191883786"}, {"skuId": "1360377703743623261"}]}
83+
}
84+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"reputationLog": {
3+
"unlocked": true,
4+
"requiredSkus": [
5+
{"id": "1380621847191883786", "name": "Supporter Starter"}, {"id": "1360377703743623261", "name": "Supporter Pro"}
6+
]
7+
},
8+
"analyzerLog": {
9+
"unlocked": true,
10+
"requiredSkus": [
11+
{"id": "1380621847191883786", "name": "Supporter Starter"}, {"id": "1360377703743623261", "name": "Supporter Pro"}
12+
]
13+
},
14+
"channelBlacklist": {
15+
"unlocked": true,
16+
"requiredSkus": [
17+
{"id": "1380621847191883786", "name": "Supporter Starter"}, {"id": "1360377703743623261", "name": "Supporter Pro"}
18+
]
19+
},
20+
"localeOverrides": {"unlocked": true, "requiredSkus": [{"id": "1360377703743623261", "name": "Supporter Pro"}]},
21+
"autopost": {
22+
"unlocked": true,
23+
"requiredSkus": [
24+
{"id": "1380621847191883786", "name": "Supporter Starter"}, {"id": "1360377703743623261", "name": "Supporter Pro"}
25+
]
26+
},
27+
"advancedRankings": {
28+
"unlocked": true,
29+
"requiredSkus": [
30+
{"id": "1380621847191883786", "name": "Supporter Starter"}, {"id": "1360377703743623261", "name": "Supporter Pro"}
31+
]
32+
},
33+
"detailedProfile": {
34+
"unlocked": true,
35+
"requiredSkus": [
36+
{"id": "1380621847191883786", "name": "Supporter Starter"}, {"id": "1360377703743623261", "name": "Supporter Pro"}
37+
]
38+
},
39+
"logChannel": {
40+
"unlocked": true,
41+
"requiredSkus": [
42+
{"id": "1380621847191883786", "name": "Supporter Starter"}, {"id": "1360377703743623261", "name": "Supporter Pro"}
43+
]
44+
},
45+
"additionalEmojis": {"unlocked": true, "requiredSkus": [{"id": "1360377703743623261", "name": "Supporter Pro"}]},
46+
"profile": {"unlocked": true, "requiredSkus": [{"id": "1360377703743623261", "name": "Supporter Pro"}]},
47+
"integrationBypass": {
48+
"unlocked": true,
49+
"requiredSkus": [
50+
{"id": "1380621847191883786", "name": "Supporter Starter"}, {"id": "1360377703743623261", "name": "Supporter Pro"}
51+
]
52+
},
53+
"reputationChannel": {
54+
"max": 2147483647,
55+
"unlocked": true,
56+
"requiredSkus": [
57+
{"id": "1380621847191883786", "name": "Supporter Starter"}, {"id": "1360377703743623261", "name": "Supporter Pro"}
58+
]
59+
},
60+
"reputationCategories": {
61+
"max": 2147483647,
62+
"unlocked": true,
63+
"requiredSkus": [
64+
{"id": "1380621847191883786", "name": "Supporter Starter"}, {"id": "1360377703743623261", "name": "Supporter Pro"}
65+
]
66+
},
67+
"activeSkus": ["1380621847191883786", "1360377703743623261"]
68+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* SPDX-License-Identifier: AGPL-3.0-only
3+
*
4+
* Copyright (C) RainbowDashLabs and Contributor
5+
*/
6+
<script lang="ts" setup>
7+
import type { SkuInfo } from '@/api/types'
8+
9+
interface Props {
10+
sku: SkuInfo
11+
active: boolean
12+
}
13+
14+
defineProps<Props>()
15+
</script>
16+
17+
<template>
18+
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-100 dark:bg-indigo-900/40 text-indigo-800 dark:text-indigo-300">
19+
{{ sku.name }}
20+
<font-awesome-icon
21+
v-if="active"
22+
icon="check-circle"
23+
class="text-green-500"
24+
/>
25+
</span>
26+
</template>

frontend/src/components/TokenFeatureCard.vue

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66
<script lang="ts" setup>
77
import {computed} from 'vue'
88
import {useI18n} from 'vue-i18n'
9-
import type {ActiveFeaturePOJO, Feature} from '@/api/types'
9+
import type {ActiveFeaturePOJO, Feature, SkuInfo} from '@/api/types'
1010
import ConfirmButton from '@/components/ConfirmButton.vue'
11+
import SkuTierBadge from '@/components/SkuTierBadge.vue'
1112
1213
const {t, d} = useI18n()
1314
1415
interface Props {
1516
feature: Feature
1617
activeFeature?: ActiveFeaturePOJO
1718
activeSkus: string[]
19+
requiredSkus: SkuInfo[]
1820
isGuildAdmin: boolean
1921
canAfford: boolean
2022
loading: boolean
@@ -52,8 +54,8 @@ const isActive = computed(() => !!props.activeFeature)
5254
const isAutoRenewal = computed(() => props.activeFeature?.autoRenewal ?? false)
5355
5456
const isEnabledBySubscription = computed(() => {
55-
if (!props.feature.skuEntry?.skus) return false
56-
return props.feature.skuEntry.skus.some(sku => props.activeSkus.includes(sku.skuId))
57+
if (!props.requiredSkus?.length) return false
58+
return props.requiredSkus.some(sku => props.activeSkus.includes(sku.id))
5759
})
5860
</script>
5961

@@ -74,6 +76,18 @@ const isEnabledBySubscription = computed(() => {
7476
</div>
7577
</div>
7678

79+
<div v-if="requiredSkus?.length" class="mb-4">
80+
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">{{ t('token_shop.requiredTiers') }}</p>
81+
<div class="flex flex-wrap gap-1">
82+
<SkuTierBadge
83+
v-for="sku in requiredSkus"
84+
:key="sku.id"
85+
:sku="sku"
86+
:active="activeSkus.includes(sku.id)"
87+
/>
88+
</div>
89+
</div>
90+
7791
<p class="text-gray-600 dark:text-gray-400 mb-6">
7892
{{ t(`${featureLocaleKey}.description`) }}
7993
</p>

frontend/src/locales/en-US.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,7 @@
944944
"subscribeSuccess": "Subscription enabled!",
945945
"unsubscribeSuccess": "Subscription disabled!",
946946
"enabledBySubscription": "Enabled by Subscription",
947+
"requiredTiers": "Supporter Tiers",
947948
"subscriptionActiveNote": "This feature is currently enabled by an active subscription.",
948949
"confirmPurchase": "Are you sure you want to purchase this feature for {amount} tokens?",
949950
"insufficientTokens": "You don't have enough tokens.",

frontend/src/views/guild/TokenShopView.vue

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import {computed, onMounted, ref} from 'vue'
88
import {useI18n} from 'vue-i18n'
99
import {api} from '@/api'
10-
import type {ActiveFeaturePOJO, Feature, PremiumFeaturesPOJO} from '@/api/types'
10+
import type {ActiveFeaturePOJO, Feature} from '@/api/types'
1111
import {useSession} from '@/composables/useSession'
1212
import TokenFeatureCard from '@/components/TokenFeatureCard.vue'
1313
import ViewContainer from '@/components/ViewContainer.vue'
@@ -17,14 +17,13 @@ import BaseButton from '@/components/BaseButton.vue'
1717
import TokenValue from '@/components/TokenValue.vue'
1818
1919
const {t} = useI18n()
20-
const {userSession, currentGuildId, userTokens, guildTokens, refreshUserTokens, refreshGuildTokens, refreshGuildPremium} = useSession()
20+
const {userSession, currentGuildId, userTokens, guildTokens, refreshUserTokens, refreshGuildTokens, refreshGuildPremium, premiumFeatures: sessionPremiumFeatures} = useSession()
2121
2222
const features = ref<Feature[]>([])
2323
const activeFeatures = ref<ActiveFeaturePOJO[]>([])
2424
const loading = ref(true)
2525
const actionLoading = ref(false)
2626
const useGuildTokens = ref(false)
27-
const premiumFeatures = ref<PremiumFeaturesPOJO | null>(null)
2827
const everyoneTokenPurchase = ref(true)
2928
const transferAmount = ref(1)
3029
const transferLoading = ref(false)
@@ -37,20 +36,17 @@ const isGuildAdmin = computed(() => {
3736
const fetchData = async (silent = false) => {
3837
if (!silent) loading.value = true
3938
try {
40-
const [featuresRes, activeRes, premiumRes, everyoneTokenPurchaseRes] = await Promise.all([
39+
const [featuresRes, activeRes, everyoneTokenPurchaseRes] = await Promise.all([
4140
api.getTokenFeatures(),
4241
api.getActiveFeatures(),
43-
refreshGuildPremium(),
4442
api.getEveryoneTokenPurchase(),
43+
refreshGuildPremium(),
4544
refreshUserTokens(),
4645
refreshGuildTokens()
4746
])
4847
49-
// api.getTokenFeatures() returns a Map-like structure or Array depending on how it was implemented/cached
50-
// In our case, the response from backend is an array of features
5148
features.value = Object.values(featuresRes)
5249
activeFeatures.value = activeRes
53-
premiumFeatures.value = premiumRes as any
5450
everyoneTokenPurchase.value = everyoneTokenPurchaseRes
5551
} catch (error) {
5652
console.error('Failed to fetch token shop data', error)
@@ -121,6 +117,31 @@ const getActiveFeature = (featureId: number) => {
121117
return activeFeatures.value.find(f => f.featureId === featureId)
122118
}
123119
120+
const getRequiredSkus = (localeKey: string) => {
121+
if (!sessionPremiumFeatures.value) return []
122+
// localeKey is like "sku.reputationlog" -> strip "sku." -> camelCase key in premiumFeatures
123+
const key = localeKey.replace('sku.', '').toLowerCase()
124+
const map: Record<string, string> = {
125+
reputationlog: 'reputationLog',
126+
analyzerlog: 'analyzerLog',
127+
channelblacklist: 'channelBlacklist',
128+
localeoverrides: 'localeOverrides',
129+
autopost: 'autopost',
130+
advancedrankings: 'advancedRankings',
131+
detailedprofile: 'detailedProfile',
132+
logchannel: 'logChannel',
133+
additionalemojis: 'additionalEmojis',
134+
profile: 'profile',
135+
integrationbypass: 'integrationBypass',
136+
reputationchannel: 'reputationChannel',
137+
reputationcategories: 'reputationCategories',
138+
}
139+
const field = map[key]
140+
if (!field) return []
141+
const feature = (sessionPremiumFeatures.value as any)[field]
142+
return feature?.requiredSkus ?? []
143+
}
144+
124145
const canAfford = (tokens: number) => {
125146
if (!everyoneTokenPurchase.value && !useGuildTokens.value && !isGuildAdmin.value) return false
126147
return (useGuildTokens.value ? guildTokens.value : userTokens.value) >= tokens
@@ -226,7 +247,8 @@ const canPurchase = computed(() => {
226247
:key="feature.id"
227248
:feature="feature"
228249
:active-feature="getActiveFeature(feature.id)"
229-
:active-skus="premiumFeatures?.activeSkus ?? []"
250+
:active-skus="[...(sessionPremiumFeatures?.activeSkus ?? [])]"
251+
:required-skus="getRequiredSkus(feature.localeKey)"
230252
:is-guild-admin="isGuildAdmin"
231253
:can-afford="canAfford(feature.tokens) && (canPurchase || useGuildTokens)"
232254
:loading="actionLoading"

src/main/java/de/chojo/repbot/config/elements/sku/SKUEntry.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public Collection<SKU> sku() {
2828

2929
@Override
3030
public String toString() {
31-
return String.join(", ", skus.stream().map(SKU::skuId).map(String::valueOf).toList());
31+
return String.join(
32+
", ", skus.stream().map(SKU::skuId).map(String::valueOf).toList());
3233
}
3334
}

src/main/java/de/chojo/repbot/web/pojo/premium/PremiumFeaturesPOJO.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,14 @@ public static PremiumFeaturesPOJO generate(RepGuild guild, ShardManager shardMan
139139

140140
List<String> active;
141141
if (TEST_MODE && GRANT_ALL_SKU) {
142-
active = guild.configuration().skus().subscriptions().stream().map(Subscription::subscriptionSku).map(String::valueOf).toList();
142+
active = guild.configuration().skus().subscriptions().stream()
143+
.map(Subscription::subscriptionSku)
144+
.map(String::valueOf)
145+
.toList();
143146
} else {
144-
active = subscriptions.sku().stream().map(s -> String.valueOf(s.skuId())).toList();
147+
active = subscriptions.sku().stream()
148+
.map(s -> String.valueOf(s.skuId()))
149+
.toList();
145150
}
146151

147152
return new PremiumFeaturesPOJO(
@@ -172,9 +177,9 @@ private static Map<Long, SkuInfo> resolveSkus(ShardManager shardManager) {
172177
shardManager.getShards().getFirst().retrieveSKUList().complete();
173178

174179
Map<Long, SkuInfo> resolvedSkus = skus.stream()
175-
.collect(Collectors.toMap(
176-
net.dv8tion.jda.api.entities.SKU::getIdLong,
177-
sku -> new SkuInfo(sku.getId(), sku.getName())));
180+
.collect(Collectors.toMap(
181+
net.dv8tion.jda.api.entities.SKU::getIdLong,
182+
sku -> new SkuInfo(sku.getId(), sku.getName())));
178183

179184
SKU_CACHE.putAll(resolvedSkus);
180185
return resolvedSkus;
@@ -193,10 +198,10 @@ private static SimpleFeature createSimpleFeature(
193198

194199
private static List<SkuInfo> extractSkuInfos(SKUEntry entry, Map<Long, SkuInfo> skuMap) {
195200
return entry.sku().stream()
196-
.map(SKU::skuId)
197-
.map(skuMap::get)
198-
.filter(Objects::nonNull)
199-
.collect(Collectors.toList());
201+
.map(SKU::skuId)
202+
.map(skuMap::get)
203+
.filter(Objects::nonNull)
204+
.collect(Collectors.toList());
200205
}
201206

202207
private static boolean isEntitled(SkuMeta subscriptions, SkuMeta required) {

0 commit comments

Comments
 (0)