Skip to content

Commit c5fb98a

Browse files
committed
ios: improve BT alert message
On iOS, Bluetooth can be “off” for two different reasons: 1. System Bluetooth is turned off 2. Bluetooth permission is disabled specifically for BitBoxApp This commit adds logic to distinguish between these cases and shows the appropriate message for each.
1 parent 8664a07 commit c5fb98a

File tree

9 files changed

+98
-18
lines changed

9 files changed

+98
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- Android: fix display of external links from Bitrefill
1111
- fix language sometimes not persistent across app restarts
1212
- Android: make the UI work with responsive font sizes and adhere to OS font size settings
13+
- iOS: show different messages when Bluetooth is off vs. when Bluetooth permission for BitBoxApp is disabled.
1314

1415
## v4.49.0
1516
- Bundle BitBox02 Nova firmware version v9.24.0

backend/backend.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ var fixedURLWhitelist = []string{
112112
"https://bitcoincore.org/en/2016/01/26/segwit-benefits/",
113113
"https://en.bitcoin.it/wiki/Bech32_adoption",
114114
"https://github.com/bitcoin/bips/",
115+
// iOS app settings
116+
"app-settings:",
115117
// Others
116118
"https://cointracking.info/import/bitbox/",
117119
}

backend/devices/bluetooth/bluetooth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ type Peripheral struct {
6969
type State struct {
7070
// BluetoothAvailable is false if bluetooth is powered off or otherwise unavailable.
7171
BluetoothAvailable bool `json:"bluetoothAvailable"`
72+
// BluetoothUnauthorized is true if the app does not have permission to use Bluetooth.
73+
BluetoothUnauthorized bool `json:"bluetoothUnauthorized"`
7274
// Scanning is true if we are currently scanning for peripherals.
7375
Scanning bool `json:"scanning"`
7476
Peripherals []*Peripheral `json:"peripherals"`

frontends/ios/BitBoxApp/BitBoxApp/BitBoxAppApp.swift

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -117,20 +117,29 @@ class GoEnvironment: NSObject, MobileserverGoEnvironmentInterfaceProtocol, UIDoc
117117

118118
func systemOpen(_ urlString: String?) throws {
119119
guard let urlString = urlString else { return }
120-
// Check if it's a local file path (not a URL)
121-
var url: URL
122-
if urlString.hasPrefix("/") {
123-
// This is a local file path, construct a file URL
124-
url = URL(fileURLWithPath: urlString)
125-
} else if let potentialURL = URL(string: urlString), potentialURL.scheme != nil {
126-
// This is already a valid URL with a scheme
127-
url = potentialURL
128-
} else {
129-
// Invalid URL or path
130-
return
131-
}
132-
// Ensure we run on the main thread
120+
133121
DispatchQueue.main.async {
122+
if urlString == "app-settings:" {
123+
if let url = URL(string: UIApplication.openSettingsURLString) {
124+
// opens app settings page in the system settings
125+
UIApplication.shared.open(url)
126+
}
127+
return
128+
}
129+
130+
// Check if it's a local file path (not a URL)
131+
var url: URL
132+
if urlString.hasPrefix("/") {
133+
// This is a local file path, construct a file URL
134+
url = URL(fileURLWithPath: urlString)
135+
} else if let potentialURL = URL(string: urlString), potentialURL.scheme != nil {
136+
// This is already a valid URL with a scheme
137+
url = potentialURL
138+
} else {
139+
// Invalid URL or path
140+
return
141+
}
142+
134143
if url.isFileURL {
135144
// Local file path, use UIDocumentInteractionController
136145
if let rootViewController = self.getRootViewController() {

frontends/ios/BitBoxApp/BitBoxApp/Bluetooth.swift

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ enum ConnectionState: String, Codable {
3030

3131
struct State {
3232
var bluetoothAvailable: Bool
33+
var bluetoothUnauthorized: Bool
3334
var scanning: Bool
3435
var discoveredPeripherals: [UUID: PeripheralMetadata]
3536
}
@@ -61,6 +62,7 @@ class BLEConnectionContext {
6162
class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
6263
private var state: State = State(
6364
bluetoothAvailable: false,
65+
bluetoothUnauthorized: false,
6466
scanning: false,
6567
discoveredPeripherals: [:]
6668
)
@@ -124,12 +126,25 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
124126

125127
func centralManagerDidUpdateState(_ central: CBCentralManager) {
126128
state.bluetoothAvailable = centralManager.state == .poweredOn
129+
130+
if #available(iOS 13.0, *) {
131+
// user denied BT permission,
132+
// or restricted by device policy
133+
let authorization = CBManager.authorization
134+
state.bluetoothUnauthorized = (authorization == .denied || authorization == .restricted)
135+
}
136+
127137
updateBackendState()
128138

129139
switch central.state {
130140
case .poweredOn:
131-
print("BLE: on")
132-
restartScan()
141+
if state.bluetoothUnauthorized {
142+
print("BLE: on but permission denied")
143+
handleDisconnect()
144+
} else {
145+
print("BLE: on")
146+
restartScan()
147+
}
133148
case .poweredOff, .unauthorized, .unsupported, .resetting, .unknown:
134149
print("BLE: unavailable or not supported")
135150
handleDisconnect()
@@ -409,6 +424,7 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
409424

410425
struct StateJSON: Codable {
411426
let bluetoothAvailable: Bool
427+
let bluetoothUnauthorized: Bool
412428
let scanning: Bool
413429
let peripherals: [PeripheralJSON]
414430
}
@@ -425,6 +441,7 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
425441

426442
let state = StateJSON(
427443
bluetoothAvailable: state.bluetoothAvailable,
444+
bluetoothUnauthorized: state.bluetoothUnauthorized,
428445
scanning: state.scanning,
429446
peripherals: peripherals
430447
)

frontends/web/src/api/bluetooth.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ export type TPeripheral = {
3131

3232
type TBluetoothState = {
3333
bluetoothAvailable: boolean;
34+
bluetoothUnauthorized: boolean;
3435
scanning: boolean;
3536
peripherals: TPeripheral[];
3637
};
3738

3839
export const getState = (): Promise<TBluetoothState> => {
40+
3941
return apiGet('bluetooth/state');
4042
};
4143

frontends/web/src/components/bluetooth/bluetooth.module.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,20 @@
2424
text-align: center;
2525
}
2626
}
27+
28+
.bluetoothDisabledContainer {
29+
align-items: start;
30+
display: flex;
31+
flex-direction: column;
32+
gap: 6px;
33+
}
34+
35+
.bluetoothDisabledTitle {
36+
font-weight: 600;
37+
}
38+
39+
.link {
40+
font-weight: 600;
41+
margin-top: var(--space-quarter);
42+
text-decoration: none;
43+
}

frontends/web/src/components/bluetooth/bluetooth.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { useSync } from '@/hooks/api';
2020
import { connect, getState, syncState, TPeripheral } from '@/api/bluetooth';
2121
import { runningInIOS } from '@/utils/env';
2222
import { Message } from '@/components/message/message';
23+
import { A } from '@/components/anchor/anchor';
2324
import { ActionableItem } from '@/components/actionable-item/actionable-item';
2425
import { Badge } from '@/components/badge/badge';
2526
import { HorizontallyCenteredSpinner, SpinnerRingAnimated } from '@/components/spinner/SpinnerAnimation';
@@ -68,10 +69,36 @@ const BluetoothInner = ({ peripheralContainerClassName }: Props) => {
6869
if (!state) {
6970
return null;
7071
}
72+
73+
if (state.bluetoothUnauthorized) {
74+
return (
75+
<Message type="warning">
76+
<div className={styles.bluetoothDisabledContainer}>
77+
<span className={styles.bluetoothDisabledTitle}>
78+
{t('bluetooth.disabledPermissionTitle')}
79+
</span>
80+
<span >
81+
{t('bluetooth.disabledPermissionDescription')}
82+
</span>
83+
<A className={styles.link} href="app-settings:">
84+
Enable
85+
</A>
86+
</div>
87+
</Message>
88+
);
89+
}
90+
7191
if (!state.bluetoothAvailable) {
7292
return (
7393
<Message type="warning">
74-
{t('bluetooth.enable')}
94+
<div className={styles.bluetoothDisabledContainer}>
95+
<span className={styles.bluetoothDisabledTitle}>
96+
{t('bluetooth.disabledGloballyTitle')}
97+
</span>
98+
<span >
99+
{t('bluetooth.disabledGloballyDescription')}
100+
</span>
101+
</div>
75102
</Message>
76103
);
77104
}

frontends/web/src/locales/en/app.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,10 @@
378378
"connected": "connected",
379379
"connectionFailed": "failed",
380380
"connectionIssues": "Having connection issues?",
381-
"enable": "Please turn on Bluetooth",
381+
"disabledGloballyDescription": "Please turn on Bluetooth to connect to your BitBox.",
382+
"disabledGloballyTitle": "Bluetooth access is disabled.",
383+
"disabledPermissionDescription": "Please allow Bluetooth to connect to your BitBox.",
384+
"disabledPermissionTitle": "Bluetooth access is disabled for BitBoxApp.",
382385
"select": "Select your BitBox"
383386
},
384387
"bootloader": {
@@ -1993,4 +1996,4 @@
19931996
"message": "Please connect your BitBox and tap the side to continue.",
19941997
"title": "Welcome"
19951998
}
1996-
}
1999+
}

0 commit comments

Comments
 (0)