Skip to content

Commit 174e42f

Browse files
committed
feat: add update channel selection for alpha/beta/stable releases
- Add generateUpdatesFilesForAllChannels to electron-builder config - Implement update channel selection in auto-updater (stable/beta/alpha) - Add IPC handlers for getting/setting update channel preference - Add Update Channel card in Settings > General with dropdown selector - Default to stable channel, users can opt-in to beta/alpha - Bump version to 1.0.0-alpha for first alpha release
1 parent 6efe222 commit 174e42f

6 files changed

Lines changed: 156 additions & 4 deletions

File tree

‎electron-builder.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@ linux:
3939
appImage:
4040
artifactName: ${name}-${version}.${ext}
4141
npmRebuild: false
42+
generateUpdatesFilesForAllChannels: true
4243
publish:
4344
provider: github

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "op-notes",
3-
"version": "1.0.0",
3+
"version": "1.0.0-alpha",
44
"description": "Op Notes",
55
"main": "./out/main/index.js",
66
"author": "Kasun Vithanage",

‎src/main/updater.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { BrowserWindow, ipcMain } from 'electron'
22
import electronUpdater, { type AppUpdater, type UpdateInfo } from 'electron-updater'
33
import { createBackup } from './backup'
4+
import { getAllSettings, updateSetting } from './repository/app-settings'
5+
6+
export type UpdateChannel = 'stable' | 'beta' | 'alpha'
47

58
export type UpdateStatus =
69
| 'idle'
@@ -25,6 +28,34 @@ export interface UpdateStatusPayload {
2528

2629
let autoUpdater: AppUpdater | null = null
2730

31+
async function getUpdateChannelFromSettings(): Promise<UpdateChannel> {
32+
try {
33+
const settings = await getAllSettings()
34+
const channelSetting = settings.find((s) => s.key === 'update_channel')
35+
const channel = channelSetting?.value as UpdateChannel
36+
if (channel && ['stable', 'beta', 'alpha'].includes(channel)) {
37+
return channel
38+
}
39+
} catch (e) {
40+
console.error('Failed to get update channel from settings:', e)
41+
}
42+
return 'stable' // default to stable
43+
}
44+
45+
async function applyUpdateChannel(): Promise<void> {
46+
if (!autoUpdater) return
47+
const channel = await getUpdateChannelFromSettings()
48+
// For stable, we use 'latest' which is the default
49+
// For beta/alpha, we set allowPrerelease and the channel
50+
if (channel === 'stable') {
51+
autoUpdater.channel = 'latest'
52+
autoUpdater.allowPrerelease = false
53+
} else {
54+
autoUpdater.channel = channel
55+
autoUpdater.allowPrerelease = true
56+
}
57+
}
58+
2859
function getMainWindow(): BrowserWindow | null {
2960
const windows = BrowserWindow.getAllWindows()
3061
return windows.length > 0 ? windows[0] : null
@@ -88,9 +119,24 @@ export function registerUpdaterIpcHandlers(): void {
88119
if (!autoUpdater) {
89120
throw new Error('Auto updater not initialized')
90121
}
122+
// Apply channel settings before checking
123+
await applyUpdateChannel()
91124
return await autoUpdater.checkForUpdates()
92125
})
93126

127+
ipcMain.handle('getUpdateChannel', async () => {
128+
return await getUpdateChannelFromSettings()
129+
})
130+
131+
ipcMain.handle('setUpdateChannel', async (_event, channel: UpdateChannel) => {
132+
if (!['stable', 'beta', 'alpha'].includes(channel)) {
133+
throw new Error('Invalid update channel')
134+
}
135+
await updateSetting('update_channel', channel)
136+
await applyUpdateChannel()
137+
return channel
138+
})
139+
94140
ipcMain.handle('downloadUpdate', async () => {
95141
if (!autoUpdater) {
96142
throw new Error('Auto updater not initialized')

‎src/preload/index.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ type ElectronApi = ElectronAPI & {
1212
downloadUpdate: () => Promise<void>
1313
quitAndInstall: () => Promise<void>
1414
onUpdateStatus: (callback: (payload: UpdateStatusPayload) => void) => () => void
15+
getUpdateChannel: () => Promise<'stable' | 'beta' | 'alpha'>
16+
setUpdateChannel: (channel: 'stable' | 'beta' | 'alpha') => Promise<string>
1517
selectBackupFolder: () => Promise<string | null>
1618
selectBackupFile: () => Promise<string | null>
1719
saveBackupAs: (

‎src/preload/index.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ const quitAndInstall = () => {
4141
return ipcRenderer.invoke('quitAndInstall')
4242
}
4343

44+
const getUpdateChannel = async (): Promise<'stable' | 'beta' | 'alpha'> => {
45+
return await ipcRenderer.invoke('getUpdateChannel')
46+
}
47+
48+
const setUpdateChannel = async (channel: 'stable' | 'beta' | 'alpha'): Promise<string> => {
49+
return await ipcRenderer.invoke('setUpdateChannel', channel)
50+
}
51+
4452
const onUpdateStatus = (callback: (payload: UpdateStatusPayload) => void) => {
4553
const handler = (_: Electron.IpcRendererEvent, payload: UpdateStatusPayload) => callback(payload)
4654
ipcRenderer.on('update-status', handler)
@@ -84,6 +92,8 @@ const electronApi = {
8492
downloadUpdate,
8593
quitAndInstall,
8694
onUpdateStatus,
95+
getUpdateChannel,
96+
setUpdateChannel,
8797
selectBackupFolder,
8898
selectBackupFile,
8999
saveBackupAs,

‎src/renderer/src/components/settings/GeneralSettings.tsx‎

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,24 @@ import {
1010
import { Input } from '@renderer/components/ui/input'
1111
import { Button } from '@renderer/components/ui/button'
1212
import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card'
13-
import { Save, RotateCcw, Building2, Phone, Building, Wand2 } from 'lucide-react'
13+
import { Save, RotateCcw, Building2, Phone, Building, Wand2, RefreshCw } from 'lucide-react'
1414
import { useForm } from 'react-hook-form'
1515
import { z } from 'zod'
1616
import { zodResolver } from '@hookform/resolvers/zod'
17-
import { useMutation, useQueryClient } from '@tanstack/react-query'
17+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
1818
import { useSettings } from '@renderer/contexts/SettingsContext'
1919
import { queries } from '@renderer/lib/queries'
2020
import { useEffect } from 'react'
2121
import { toast } from '@renderer/components/ui/sonner'
22+
import {
23+
Select,
24+
SelectContent,
25+
SelectItem,
26+
SelectTrigger,
27+
SelectValue
28+
} from '@renderer/components/ui/select'
29+
30+
type UpdateChannel = 'stable' | 'beta' | 'alpha'
2231

2332
const formSchema = z.object({
2433
hospital: z.string(),
@@ -40,6 +49,22 @@ export const GeneralSettings = () => {
4049
}
4150
})
4251

52+
const { data: updateChannel = 'stable' } = useQuery({
53+
queryKey: ['updateChannel'],
54+
queryFn: () => window.electronApi.getUpdateChannel()
55+
})
56+
57+
const updateChannelMutation = useMutation({
58+
mutationFn: (channel: UpdateChannel) => window.electronApi.setUpdateChannel(channel),
59+
onSuccess: () => {
60+
queryClient.invalidateQueries({ queryKey: ['updateChannel'] })
61+
toast.success('Update channel changed')
62+
},
63+
onError: (error: Error) => {
64+
toast.error(error.message)
65+
}
66+
})
67+
4368
useEffect(() => {
4469
if (settings) {
4570
form.reset({
@@ -193,10 +218,78 @@ export const GeneralSettings = () => {
193218
</CardContent>
194219
</Card>
195220

221+
{/* Update Channel Card */}
222+
<Card
223+
className="bg-gradient-to-br from-card to-card/80 animate-fade-in-up overflow-hidden"
224+
style={{ animationDelay: '75ms' }}
225+
>
226+
<CardHeader className="pb-4 pt-5 relative">
227+
<div className="absolute top-0 right-0 w-32 h-32 bg-green-500/5 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl" />
228+
<div className="flex items-center gap-3 relative">
229+
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-green-500/20 to-green-600/20 flex items-center justify-center border border-green-500/20">
230+
<RefreshCw className="h-5 w-5 text-green-500" />
231+
</div>
232+
<div>
233+
<CardTitle className="text-base font-semibold">Update Channel</CardTitle>
234+
<p className="text-xs text-muted-foreground mt-0.5">
235+
Choose which updates you receive
236+
</p>
237+
</div>
238+
</div>
239+
</CardHeader>
240+
241+
<CardContent className="pt-0">
242+
<div className="space-y-4">
243+
<div>
244+
<label className="text-sm font-medium mb-2 block">Release Channel</label>
245+
<Select
246+
value={updateChannel}
247+
onValueChange={(value: UpdateChannel) => updateChannelMutation.mutate(value)}
248+
disabled={updateChannelMutation.isPending}
249+
>
250+
<SelectTrigger className="w-full md:w-64 h-11">
251+
<SelectValue placeholder="Select channel" />
252+
</SelectTrigger>
253+
<SelectContent>
254+
<SelectItem value="stable">
255+
<div className="flex flex-col items-start">
256+
<span className="font-medium">Stable</span>
257+
<span className="text-xs text-muted-foreground">
258+
Recommended for most users
259+
</span>
260+
</div>
261+
</SelectItem>
262+
<SelectItem value="beta">
263+
<div className="flex flex-col items-start">
264+
<span className="font-medium">Beta</span>
265+
<span className="text-xs text-muted-foreground">
266+
Preview upcoming features
267+
</span>
268+
</div>
269+
</SelectItem>
270+
<SelectItem value="alpha">
271+
<div className="flex flex-col items-start">
272+
<span className="font-medium">Alpha</span>
273+
<span className="text-xs text-muted-foreground">
274+
Bleeding edge, may be unstable
275+
</span>
276+
</div>
277+
</SelectItem>
278+
</SelectContent>
279+
</Select>
280+
<p className="text-xs text-muted-foreground mt-2">
281+
Stable releases are thoroughly tested. Beta and alpha channels receive updates
282+
earlier but may contain bugs.
283+
</p>
284+
</div>
285+
</div>
286+
</CardContent>
287+
</Card>
288+
196289
{/* Setup Wizard Card */}
197290
<Card
198291
className="bg-gradient-to-br from-amber-500/5 to-amber-600/5 border-amber-500/20 animate-fade-in-up"
199-
style={{ animationDelay: '75ms' }}
292+
style={{ animationDelay: '150ms' }}
200293
>
201294
<CardHeader className="pb-3 pt-4">
202295
<div className="flex items-center gap-2.5">

0 commit comments

Comments
 (0)