Skip to content

Commit 3bd774c

Browse files
authored
Merge pull request #3 from decentraland/feat/add-version-tab
feat: add version tab,
1 parent 8d84b10 commit 3bd774c

5 files changed

Lines changed: 1137 additions & 1 deletion

File tree

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { AppTabs, type AppView } from './components/AppTabs'
88
import { MapPage } from './pages/MapPage'
99
import { WorldsPage } from './pages/WorldsPage'
1010
import { CurationPage } from './pages/CurationPage'
11+
import { VersionsPage } from './pages/VersionsPage'
1112
import { config } from './config'
1213

1314
const authConfig = {
@@ -30,6 +31,7 @@ function App() {
3031
{activeView === 'map' && <MapPage />}
3132
{activeView === 'worlds' && <WorldsPage />}
3233
{activeView === 'curation' && <CurationPage />}
34+
{activeView === 'versions' && <VersionsPage />}
3335
</BansProvider>
3436
</AuthProvider>
3537
</BrowserRouter>

src/components/AppTabs/AppTabs.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { FC } from 'react'
22
import './AppTabs.css'
33

4-
export type AppView = 'map' | 'worlds' | 'curation'
4+
export type AppView = 'map' | 'worlds' | 'curation' | 'versions'
55

66
interface AppTabsProps {
77
activeView: AppView
@@ -29,6 +29,12 @@ export const AppTabs: FC<AppTabsProps> = ({ activeView, onViewChange }) => {
2929
>
3030
Curation
3131
</button>
32+
<button
33+
className={`app-tab ${activeView === 'versions' ? 'app-tab-active' : ''}`}
34+
onClick={() => onViewChange('versions')}
35+
>
36+
Versions
37+
</button>
3238
</div>
3339
)
3440
}

src/features/versions/api.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { config } from '../../config'
2+
3+
const API_BASE = config.get('MOBILE_BFF_URL')
4+
5+
const GODOT_EXPLORER_RAW =
6+
'https://raw.githubusercontent.com/decentraland/godot-explorer'
7+
const GODOT_EXPORT_PRESETS_PATH = 'godot/export_presets.cfg'
8+
9+
export interface PlatformVersions {
10+
minimalRequiredVersionNumber: number
11+
recommendedVersionNumber: number
12+
}
13+
14+
export interface AppVersions {
15+
ios: PlatformVersions
16+
android: PlatformVersions
17+
}
18+
19+
interface ApiResponse<T> {
20+
ok: boolean
21+
data?: T
22+
error?: string
23+
}
24+
25+
export async function fetchAppVersions(): Promise<AppVersions> {
26+
const response = await fetch(`${API_BASE}/app-versions`)
27+
const json: ApiResponse<AppVersions> = await response.json()
28+
29+
if (!json.ok || !json.data) {
30+
throw new Error(json.error || 'Failed to fetch app versions')
31+
}
32+
33+
return json.data
34+
}
35+
36+
export async function updateAppVersions(
37+
authenticatedFetch: (url: string, init?: RequestInit) => Promise<Response>,
38+
values: AppVersions
39+
): Promise<AppVersions> {
40+
const response = await authenticatedFetch(`${API_BASE}/backoffice/app-versions`, {
41+
method: 'PUT',
42+
headers: { 'Content-Type': 'application/json' },
43+
body: JSON.stringify(values),
44+
})
45+
const json: ApiResponse<AppVersions> = await response.json()
46+
47+
if (!json.ok || !json.data) {
48+
throw new Error(json.error || 'Failed to update app versions')
49+
}
50+
51+
return json.data
52+
}
53+
54+
export interface PlatformCurrentVersion {
55+
versionNumber: number | null
56+
displayVersion: string | null
57+
}
58+
59+
export interface BranchVersions {
60+
ios: PlatformCurrentVersion
61+
android: PlatformCurrentVersion
62+
}
63+
64+
export type GodotExplorerBranch = 'main' | 'release'
65+
66+
/**
67+
* Parse export_presets.cfg into section → key/value map.
68+
* The file uses INI-like syntax with [section.name] headers.
69+
*/
70+
function parseExportPresets(text: string): Record<string, Record<string, string>> {
71+
const sections: Record<string, Record<string, string>> = {}
72+
let currentSection: string | null = null
73+
74+
for (const rawLine of text.split(/\r?\n/)) {
75+
const line = rawLine.trim()
76+
if (!line || line.startsWith(';') || line.startsWith('#')) continue
77+
78+
const sectionMatch = line.match(/^\[(.+)\]$/)
79+
if (sectionMatch) {
80+
currentSection = sectionMatch[1]
81+
sections[currentSection] ??= {}
82+
continue
83+
}
84+
85+
if (!currentSection) continue
86+
87+
const eqIdx = line.indexOf('=')
88+
if (eqIdx === -1) continue
89+
90+
const key = line.slice(0, eqIdx).trim()
91+
const value = line.slice(eqIdx + 1).trim()
92+
sections[currentSection][key] = value
93+
}
94+
95+
return sections
96+
}
97+
98+
function stripQuotes(value: string | undefined): string | null {
99+
if (value === undefined) return null
100+
const m = value.match(/^"(.*)"$/)
101+
return m ? m[1] : value
102+
}
103+
104+
function parseIntOrNull(value: string | undefined): number | null {
105+
if (value === undefined) return null
106+
const unquoted = stripQuotes(value) ?? value
107+
const n = parseInt(unquoted, 10)
108+
return Number.isFinite(n) ? n : null
109+
}
110+
111+
/**
112+
* Extract iOS + Android version info from export_presets.cfg text.
113+
* Assumes typical Godot preset layout with named "android" and "ios" platforms.
114+
*/
115+
function extractVersionsFromPresets(text: string): BranchVersions {
116+
const sections = parseExportPresets(text)
117+
118+
let android: PlatformCurrentVersion = { versionNumber: null, displayVersion: null }
119+
let ios: PlatformCurrentVersion = { versionNumber: null, displayVersion: null }
120+
121+
for (const [sectionName, fields] of Object.entries(sections)) {
122+
const platformName = stripQuotes(fields['name'])
123+
if (!platformName) continue
124+
125+
const optionsSection = sections[`${sectionName}.options`] ?? {}
126+
127+
if (platformName === 'android') {
128+
android = {
129+
versionNumber: parseIntOrNull(optionsSection['version/code']),
130+
displayVersion: stripQuotes(optionsSection['version/name']),
131+
}
132+
} else if (platformName === 'ios') {
133+
ios = {
134+
versionNumber: parseIntOrNull(optionsSection['application/version']),
135+
displayVersion: stripQuotes(optionsSection['application/short_version']),
136+
}
137+
}
138+
}
139+
140+
return { ios, android }
141+
}
142+
143+
export async function fetchGodotExplorerVersions(
144+
branch: GodotExplorerBranch
145+
): Promise<BranchVersions> {
146+
const url = `${GODOT_EXPLORER_RAW}/${branch}/${GODOT_EXPORT_PRESETS_PATH}`
147+
const response = await fetch(url, { cache: 'no-store' })
148+
149+
if (!response.ok) {
150+
throw new Error(
151+
`Failed to fetch godot-explorer ${branch} export_presets.cfg (${response.status})`
152+
)
153+
}
154+
155+
const text = await response.text()
156+
return extractVersionsFromPresets(text)
157+
}

0 commit comments

Comments
 (0)