Skip to content

Commit c91a31d

Browse files
committed
Ideal foundation: iOS 17, modern home, subscribe sheet
Raise the deployment floor to iOS 17, switch to NavigationStack, refresh brand type and panels, add home empty-state CTAs, a SwiftUI subscribe sheet for crypt/https links, and a real Xray running session. Document the Ideal roadmap in docs/IDEAL.md for the next phases.
1 parent a735fc3 commit c91a31d

16 files changed

Lines changed: 653 additions & 171 deletions

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,20 @@ All notable changes to **decoder sec.** are documented here.
66

77
### Planned
88
- Subscription UX (node list / one-tap connect)
9+
- In-app log console
910
- Signed TestFlight builds
1011

12+
## [0.1.0-beta.10] — 2026-08-11
13+
14+
### Changed
15+
- Minimum iOS **17.0**; marketing version aligned to `0.1.0`
16+
- `NavigationStack`, modern `onChange`, ContentUnavailable empty states
17+
- Brand type: rounded display + mono chrome; quieter neon glow; iOS 26 panel material hook
18+
- Home empty state with **Add subscription** CTA
19+
- Subscribe sheet (SwiftUI) for https / `happ://crypt…` / share links
20+
- Xray running session shows brand + active config (not a blank stub)
21+
- Settings show app version; Ideal roadmap in `docs/IDEAL.md`
22+
1123
## [0.1.0-beta.9] — 2026-08-11
1224

1325
### Fixed

‎DecoderSec.xcodeproj/project.pbxproj‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@
363363
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
364364
GCC_WARN_UNUSED_FUNCTION = YES;
365365
GCC_WARN_UNUSED_VARIABLE = YES;
366-
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
366+
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
367367
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
368368
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
369369
MTL_FAST_MATH = YES;
@@ -424,7 +424,7 @@
424424
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
425425
GCC_WARN_UNUSED_FUNCTION = YES;
426426
GCC_WARN_UNUSED_VARIABLE = YES;
427-
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
427+
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
428428
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
429429
MTL_ENABLE_DEBUG_INFO = NO;
430430
MTL_FAST_MATH = YES;
@@ -456,12 +456,12 @@
456456
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
457457
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
458458
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
459-
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
459+
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
460460
LD_RUNPATH_SEARCH_PATHS = (
461461
"$(inherited)",
462462
"@executable_path/Frameworks",
463463
);
464-
MARKETING_VERSION = 1.5;
464+
MARKETING_VERSION = 0.1.0;
465465
PRODUCT_BUNDLE_IDENTIFIER = com.decodersec.app;
466466
PRODUCT_NAME = "$(TARGET_NAME)";
467467
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -494,12 +494,12 @@
494494
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
495495
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
496496
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
497-
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
497+
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
498498
LD_RUNPATH_SEARCH_PATHS = (
499499
"$(inherited)",
500500
"@executable_path/Frameworks",
501501
);
502-
MARKETING_VERSION = 1.5;
502+
MARKETING_VERSION = 0.1.0;
503503
PRODUCT_BUNDLE_IDENTIFIER = com.decodersec.app;
504504
PRODUCT_NAME = "$(TARGET_NAME)";
505505
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -523,13 +523,13 @@
523523
INFOPLIST_FILE = DecoderSecTunnel/Info.plist;
524524
INFOPLIST_KEY_CFBundleDisplayName = "decoder sec. Tunnel";
525525
INFOPLIST_KEY_NSHumanReadableCopyright = "";
526-
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
526+
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
527527
LD_RUNPATH_SEARCH_PATHS = (
528528
"$(inherited)",
529529
"@executable_path/Frameworks",
530530
"@executable_path/../../Frameworks",
531531
);
532-
MARKETING_VERSION = 1.5;
532+
MARKETING_VERSION = 0.1.0;
533533
PRODUCT_BUNDLE_IDENTIFIER = com.decodersec.app.PacketTunnel;
534534
PRODUCT_NAME = "$(TARGET_NAME)";
535535
SKIP_INSTALL = YES;
@@ -553,13 +553,13 @@
553553
INFOPLIST_FILE = DecoderSecTunnel/Info.plist;
554554
INFOPLIST_KEY_CFBundleDisplayName = "decoder sec. Tunnel";
555555
INFOPLIST_KEY_NSHumanReadableCopyright = "";
556-
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
556+
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
557557
LD_RUNPATH_SEARCH_PATHS = (
558558
"$(inherited)",
559559
"@executable_path/Frameworks",
560560
"@executable_path/../../Frameworks",
561561
);
562-
MARKETING_VERSION = 1.5;
562+
MARKETING_VERSION = 0.1.0;
563563
PRODUCT_BUNDLE_IDENTIFIER = com.decodersec.app.PacketTunnel;
564564
PRODUCT_NAME = "$(TARGET_NAME)";
565565
SKIP_INSTALL = YES;

‎DecoderSec/Configurations/ConfigurationsView.swift‎

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ struct ConfigurationsView: View {
1616
@State private var fileImporting = false
1717
@State private var isDownloading = false
1818
@State private var importErrorMessage: String?
19+
@State private var showSubscribe = false
1920

2021
private var activeID: UUID? { store.activeIDByCoreType[store.selectedCore] }
2122

@@ -50,6 +51,18 @@ struct ConfigurationsView: View {
5051
}
5152
}
5253
}
54+
.overlay {
55+
if store.configurationsForSelectedCore.isEmpty {
56+
ContentUnavailableView {
57+
Label(String(localized: "No configurations"), systemImage: "doc.badge.plus")
58+
} description: {
59+
Text(String(localized: "Create one, import a file, or add a subscription."))
60+
} actions: {
61+
Button(String(localized: "Subscribe")) { showSubscribe = true }
62+
.buttonStyle(.borderedProminent)
63+
}
64+
}
65+
}
5366
.navigationTitle("\(store.selectedCore.displayName) configurations")
5467
.navigationBarTitleDisplayMode(.inline)
5568
.toolbar {
@@ -69,7 +82,7 @@ struct ConfigurationsView: View {
6982
Label("Import from file", systemImage: "doc")
7083
}
7184
Button {
72-
promptSubscribe()
85+
showSubscribe = true
7386
} label: {
7487
Label("Subscribe", systemImage: "link")
7588
}
@@ -79,6 +92,9 @@ struct ConfigurationsView: View {
7992
}
8093
}
8194
}
95+
.sheet(isPresented: $showSubscribe) {
96+
SubscribeSheet()
97+
}
8298
.fileImporter(
8399
isPresented: $fileImporting,
84100
allowedContentTypes: [.json, .yaml, .text, .data, .item],
@@ -181,16 +197,6 @@ struct ConfigurationsView: View {
181197
}
182198
}
183199

184-
private func promptSubscribe() {
185-
let core = store.selectedCore
186-
URLInputAlert.present(
187-
title: String(localized: "Subscribe to \(core.displayName) configuration"),
188-
message: String(localized: "Enter an https subscription URL or a happ://crypt… / share link.")
189-
) { url in
190-
importFromSubscribeField(url)
191-
}
192-
}
193-
194200
private func extractRemarks(from content: String, fallbackUrl: URL) -> String {
195201
// JSON
196202
if let data = content.data(using: .utf8),
@@ -221,23 +227,6 @@ struct ConfigurationsView: View {
221227
}
222228
}
223229

224-
/// Subscribe field: https → HWID fetch; happ://crypt5 / share links → DeepLinkCenter.
225-
private func importFromSubscribeField(_ url: URL) {
226-
isDownloading = true
227-
Task { @MainActor in
228-
defer { isDownloading = false }
229-
let scheme = (url.scheme ?? "").lowercased()
230-
if scheme == "http" || scheme == "https" {
231-
await DeepLinkCenter.shared.perform(.addSubscription(url))
232-
} else {
233-
await DeepLinkCenter.shared.handleAsync(url)
234-
}
235-
if let err = DeepLinkCenter.shared.lastError {
236-
importErrorMessage = err
237-
}
238-
}
239-
}
240-
241230
private func updateSubscription(_ config: Configuration) {
242231
guard let raw = config.sourceURL, let url = URL(string: raw) else { return }
243232
isDownloading = true
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//
2+
// SubscribeSheet.swift
3+
// DecoderSec
4+
//
5+
// Modern subscribe UI: https URLs, happ://crypt…, and share links.
6+
//
7+
8+
import SwiftUI
9+
10+
struct SubscribeSheet: View {
11+
@Environment(\.dismiss) private var dismiss
12+
@ObservedObject private var deepLinks = DeepLinkCenter.shared
13+
@State private var raw = ""
14+
@State private var localError: String?
15+
@FocusState private var focused: Bool
16+
17+
var body: some View {
18+
NavigationStack {
19+
VStack(alignment: .leading, spacing: 20) {
20+
Text(String(localized: "Paste a subscription URL, happ://crypt… link, or vless:// node."))
21+
.font(Brand.Font.body(15))
22+
.foregroundStyle(Brand.Color.secondaryText)
23+
24+
TextField(String(localized: "https://… or happ://crypt5/…"), text: $raw, axis: .vertical)
25+
.font(Brand.Font.mono(13))
26+
.foregroundStyle(Brand.Color.primaryText)
27+
.lineLimit(4...8)
28+
.padding(14)
29+
.brandPanel(cornerRadius: 12)
30+
.focused($focused)
31+
.textInputAutocapitalization(.never)
32+
.autocorrectionDisabled()
33+
.keyboardType(.URL)
34+
35+
if let localError {
36+
Text(localError)
37+
.font(Brand.Font.callout(13))
38+
.foregroundStyle(Brand.Color.danger)
39+
}
40+
41+
Spacer(minLength: 0)
42+
}
43+
.padding(20)
44+
.background(Brand.Color.void.ignoresSafeArea())
45+
.navigationTitle(String(localized: "Subscribe"))
46+
.navigationBarTitleDisplayMode(.inline)
47+
.toolbar {
48+
ToolbarItem(placement: .cancellationAction) {
49+
Button(String(localized: "Cancel")) { dismiss() }
50+
}
51+
ToolbarItem(placement: .confirmationAction) {
52+
Button(String(localized: "Import")) {
53+
Task { await submit() }
54+
}
55+
.disabled(raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || deepLinks.isBusy)
56+
.fontWeight(.semibold)
57+
}
58+
}
59+
.overlay {
60+
if deepLinks.isBusy {
61+
ProgressView()
62+
.tint(Brand.Color.neon)
63+
.padding(20)
64+
.background(Brand.Color.surface.opacity(0.95), in: RoundedRectangle(cornerRadius: 14, style: .continuous))
65+
}
66+
}
67+
.onAppear { focused = true }
68+
}
69+
.presentationDetents([.medium, .large])
70+
.brandTheme()
71+
}
72+
73+
@MainActor
74+
private func submit() async {
75+
localError = nil
76+
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
77+
guard let url = Self.parse(trimmed) else {
78+
localError = String(localized: "Unrecognized link. Use https://, happ://crypt…, or a share link.")
79+
return
80+
}
81+
let scheme = (url.scheme ?? "").lowercased()
82+
if scheme == "http" || scheme == "https" {
83+
await deepLinks.perform(.addSubscription(url))
84+
} else {
85+
await deepLinks.handleAsync(url)
86+
}
87+
if let err = deepLinks.lastError {
88+
localError = err
89+
return
90+
}
91+
dismiss()
92+
}
93+
94+
static func parse(_ raw: String) -> URL? {
95+
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
96+
guard !trimmed.isEmpty else { return nil }
97+
let url = URL(string: trimmed)
98+
?? URL(string: trimmed.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "")
99+
guard let url, let scheme = url.scheme?.lowercased() else { return nil }
100+
if scheme == "http" || scheme == "https" {
101+
guard let host = url.host, !host.isEmpty else { return nil }
102+
return url
103+
}
104+
if HappDeepLink.aliasSchemes.contains(scheme) { return url }
105+
if HappDeepLink.shareSchemes.contains(scheme) { return url }
106+
return nil
107+
}
108+
}

0 commit comments

Comments
 (0)