Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- Add icons for CTA and action buttons in account page
- Restructure "Manage device" tab in settings
- Responsive account selector (Marketplace)
- iOS: show different messages when Bluetooth is off vs. when Bluetooth permission for BitBoxApp is disabled.

## v4.49.0
- Bundle BitBox02 Nova firmware version v9.24.0
Expand Down
2 changes: 2 additions & 0 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ var fixedURLWhitelist = []string{
"https://bitcoincore.org/en/2016/01/26/segwit-benefits/",
"https://en.bitcoin.it/wiki/Bech32_adoption",
"https://github.com/bitcoin/bips/",
// iOS app settings
"app-settings:",
// Others
"https://cointracking.info/import/bitbox/",
}
Expand Down
2 changes: 2 additions & 0 deletions backend/devices/bluetooth/bluetooth.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ type Peripheral struct {
type State struct {
// BluetoothAvailable is false if bluetooth is powered off or otherwise unavailable.
BluetoothAvailable bool `json:"bluetoothAvailable"`
// BluetoothUnauthorized is true if the app does not have permission to use Bluetooth.
BluetoothUnauthorized bool `json:"bluetoothUnauthorized"`
// Scanning is true if we are currently scanning for peripherals.
Scanning bool `json:"scanning"`
Peripherals []*Peripheral `json:"peripherals"`
Expand Down
35 changes: 22 additions & 13 deletions frontends/ios/BitBoxApp/BitBoxApp/BitBoxAppApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,20 +117,29 @@ class GoEnvironment: NSObject, MobileserverGoEnvironmentInterfaceProtocol, UIDoc

func systemOpen(_ urlString: String?) throws {
guard let urlString = urlString else { return }
// Check if it's a local file path (not a URL)
var url: URL
if urlString.hasPrefix("/") {
// This is a local file path, construct a file URL
url = URL(fileURLWithPath: urlString)
} else if let potentialURL = URL(string: urlString), potentialURL.scheme != nil {
// This is already a valid URL with a scheme
url = potentialURL
} else {
// Invalid URL or path
return
}
// Ensure we run on the main thread

DispatchQueue.main.async {
if urlString == "app-settings:" {
if let url = URL(string: UIApplication.openSettingsURLString) {
// opens app settings page in the system settings
UIApplication.shared.open(url)
}
return
}

// Check if it's a local file path (not a URL)
var url: URL
if urlString.hasPrefix("/") {
// This is a local file path, construct a file URL
url = URL(fileURLWithPath: urlString)
} else if let potentialURL = URL(string: urlString), potentialURL.scheme != nil {
// This is already a valid URL with a scheme
url = potentialURL
} else {
// Invalid URL or path
return
}

if url.isFileURL {
// Local file path, use UIDocumentInteractionController
if let rootViewController = self.getRootViewController() {
Expand Down
21 changes: 19 additions & 2 deletions frontends/ios/BitBoxApp/BitBoxApp/Bluetooth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ enum ConnectionState: String, Codable {

struct State {
var bluetoothAvailable: Bool
var bluetoothUnauthorized: Bool
var scanning: Bool
var discoveredPeripherals: [UUID: PeripheralMetadata]
}
Expand Down Expand Up @@ -61,6 +62,7 @@ class BLEConnectionContext {
class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
private var state: State = State(
bluetoothAvailable: false,
bluetoothUnauthorized: false,
scanning: false,
discoveredPeripherals: [:]
)
Expand Down Expand Up @@ -124,12 +126,25 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB

func centralManagerDidUpdateState(_ central: CBCentralManager) {
state.bluetoothAvailable = centralManager.state == .poweredOn

if #available(iOS 13.0, *) {
// user denied BT permission,
// or restricted by device policy
let authorization = CBManager.authorization
state.bluetoothUnauthorized = (authorization == .denied || authorization == .restricted)
}

updateBackendState()

switch central.state {
case .poweredOn:
print("BLE: on")
restartScan()
if state.bluetoothUnauthorized {
print("BLE: on but permission denied")
handleDisconnect()
} else {
print("BLE: on")
restartScan()
}
case .poweredOff, .unauthorized, .unsupported, .resetting, .unknown:
print("BLE: unavailable or not supported")
handleDisconnect()
Expand Down Expand Up @@ -409,6 +424,7 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB

struct StateJSON: Codable {
let bluetoothAvailable: Bool
let bluetoothUnauthorized: Bool
let scanning: Bool
let peripherals: [PeripheralJSON]
}
Expand All @@ -425,6 +441,7 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB

let state = StateJSON(
bluetoothAvailable: state.bluetoothAvailable,
bluetoothUnauthorized: state.bluetoothUnauthorized,
scanning: state.scanning,
peripherals: peripherals
)
Expand Down
2 changes: 2 additions & 0 deletions frontends/web/src/api/bluetooth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ export type TPeripheral = {

type TBluetoothState = {
bluetoothAvailable: boolean;
bluetoothUnauthorized: boolean;
scanning: boolean;
peripherals: TPeripheral[];
};

export const getState = (): Promise<TBluetoothState> => {

return apiGet('bluetooth/state');
};

Expand Down
17 changes: 17 additions & 0 deletions frontends/web/src/components/bluetooth/bluetooth.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,20 @@
text-align: center;
}
}

.bluetoothDisabledContainer {
align-items: start;
display: flex;
flex-direction: column;
gap: 6px;
}

.bluetoothDisabledTitle {
font-weight: 600;
}

.link {
font-weight: 600;
margin-top: var(--space-quarter);
text-decoration: none;
}
29 changes: 28 additions & 1 deletion frontends/web/src/components/bluetooth/bluetooth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useSync } from '@/hooks/api';
import { connect, getState, syncState, TPeripheral } from '@/api/bluetooth';
import { runningInIOS } from '@/utils/env';
import { Message } from '@/components/message/message';
import { A } from '@/components/anchor/anchor';
import { ActionableItem } from '@/components/actionable-item/actionable-item';
import { Badge } from '@/components/badge/badge';
import { HorizontallyCenteredSpinner, SpinnerRingAnimated } from '@/components/spinner/SpinnerAnimation';
Expand Down Expand Up @@ -54,10 +55,36 @@ const BluetoothInner = ({ peripheralContainerClassName }: Props) => {
if (!state) {
return null;
}

if (state.bluetoothUnauthorized) {
return (
<Message type="warning">
<div className={styles.bluetoothDisabledContainer}>
<span className={styles.bluetoothDisabledTitle}>
{t('bluetooth.disabledPermissionTitle')}
</span>
<span >
{t('bluetooth.disabledPermissionDescription')}
</span>
<A className={styles.link} href="app-settings:">
{t('generic.enable')}
</A>
</div>
</Message>
);
}

if (!state.bluetoothAvailable) {
return (
<Message type="warning">
{t('bluetooth.enable')}
<div className={styles.bluetoothDisabledContainer}>
<span className={styles.bluetoothDisabledTitle}>
{t('bluetooth.disabledGloballyTitle')}
</span>
<span >
{t('bluetooth.disabledGloballyDescription')}
</span>
</div>
</Message>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ export type TOption = TDropdownOption<AccountCode> & TOptionAccountSelector;
export type TGroupedOption = TDropdownGroupedOption<AccountCode, TGroupAccountSelector, TOptionAccountSelector>;

type TAccountSelector = {
title: string;
title?: string;
disabled?: boolean;
selected?: string;
onChange: (value: string) => void;
onProceed: () => void;
onProceed?: () => void;
accounts: TAccount[];
};

Expand Down Expand Up @@ -120,7 +120,9 @@ export const GroupedAccountSelector = ({ title, disabled, selected, onChange, on

return (
<>
<h1 className="title text-center">{title}</h1>
{title && (
<h1 className="title text-center">{title}</h1>
)}
<Dropdown<AccountCode, false, TGroupAccountSelector, TOptionAccountSelector>
className={styles.select}
classNamePrefix="react-select"
Expand All @@ -139,14 +141,16 @@ export const GroupedAccountSelector = ({ title, disabled, selected, onChange, on
onOpenChange={setIsOpen}
mobileTriggerComponent={mobileTriggerComponent}
/>
<div className="buttons text-center">
<Button
primary
onClick={onProceed}
disabled={!selected || disabled}>
{t('buy.info.next')}
</Button>
</div>
{onProceed && (
<div className="buttons text-center">
<Button
primary
onClick={onProceed}
disabled={!selected || disabled}>
{t('buy.info.next')}
</Button>
</div>
)}
</>
);
};
};
6 changes: 5 additions & 1 deletion frontends/web/src/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,10 @@
"connected": "connected",
"connectionFailed": "failed",
"connectionIssues": "Having connection issues?",
"enable": "Please turn on Bluetooth",
"disabledGloballyDescription": "Please turn on Bluetooth to connect to your BitBox.",
"disabledGloballyTitle": "Bluetooth access is disabled.",
"disabledPermissionDescription": "Please allow Bluetooth to connect to your BitBox.",
"disabledPermissionTitle": "Bluetooth access is disabled for BitBoxApp.",
"select": "Select your BitBox"
},
"bootloader": {
Expand Down Expand Up @@ -891,6 +894,7 @@
"buy_bitcoin": "Buy Bitcoin",
"buy_crypto": "Buy crypto",
"close": "Close",
"enable": "Enable",
"enabled_false": "Disabled",
"enabled_true": "Enabled",
"noOptionOnIos": "Not available on iOS",
Expand Down