Skip to content

Commit 5e46f75

Browse files
JOHNJOHN
authored andcommitted
feat: add PubkyRing auth screen navigation
1 parent 5a4032f commit 5e46f75

File tree

3 files changed

+134
-41
lines changed

3 files changed

+134
-41
lines changed

app/src/main/java/to/bitkit/paykit/services/PubkyRingBridge.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,37 @@ class PubkyRingBridge @Inject constructor(
456456
throw PubkyRingException.Timeout
457457
}
458458

459+
/**
460+
* Handle an authentication URL from a scanned QR code or pasted link
461+
*
462+
* Parses URLs in the format:
463+
* - pubky://session?pubkey=...&session_secret=...
464+
* - pubkyring://session?pubkey=...&session_secret=...
465+
* - bitkit://paykit-session?pubkey=...&session_secret=...
466+
*
467+
* @param url The URL to parse
468+
* @return PubkySession if successful
469+
* @throws PubkyRingException if URL is invalid
470+
*/
471+
fun handleAuthUrl(url: String): PubkySession {
472+
val uri = Uri.parse(url)
473+
474+
// Check if it's a callback URL with session data
475+
val pubkey = uri.getQueryParameter("pubkey") ?: uri.getQueryParameter("pk")
476+
val sessionSecret = uri.getQueryParameter("session_secret") ?: uri.getQueryParameter("ss")
477+
478+
if (pubkey != null && sessionSecret != null) {
479+
val capabilities = uri.getQueryParameter("capabilities")
480+
?.split(",")
481+
?.filter { it.isNotEmpty() }
482+
?: emptyList()
483+
484+
return importSession(pubkey, sessionSecret, capabilities)
485+
}
486+
487+
throw PubkyRingException.InvalidCallback
488+
}
489+
459490
/**
460491
* Import a session manually (for offline/manual cross-device flow)
461492
*

app/src/main/java/to/bitkit/paykit/ui/PubkyRingAuthScreen.kt

Lines changed: 82 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import androidx.compose.foundation.verticalScroll
1919
import androidx.compose.material.icons.Icons
2020
import androidx.compose.material.icons.automirrored.filled.ArrowBack
2121
import androidx.compose.material.icons.filled.ContentCopy
22-
import androidx.compose.material.icons.filled.Keyboard
2322
import androidx.compose.material.icons.filled.Link
2423
import androidx.compose.material.icons.filled.QrCode
2524
import androidx.compose.material.icons.filled.Refresh
@@ -55,7 +54,6 @@ import androidx.compose.ui.platform.LocalClipboardManager
5554
import androidx.compose.ui.platform.LocalContext
5655
import androidx.compose.ui.text.AnnotatedString
5756
import androidx.compose.ui.text.font.FontWeight
58-
import androidx.compose.ui.text.input.PasswordVisualTransformation
5957
import androidx.compose.ui.text.style.TextAlign
6058
import androidx.compose.ui.unit.dp
6159
import kotlinx.coroutines.delay
@@ -70,6 +68,8 @@ import to.bitkit.ui.theme.Colors
7068
fun PubkyRingAuthScreen(
7169
onNavigateBack: () -> Unit,
7270
onSessionReceived: (PubkySession) -> Unit,
71+
onNavigateToScanner: (() -> Unit)? = null,
72+
scannedQrCode: String? = null,
7373
modifier: Modifier = Modifier,
7474
) {
7575
val context = LocalContext.current
@@ -85,10 +85,27 @@ fun PubkyRingAuthScreen(
8585
var manualPubkey by remember { mutableStateOf("") }
8686
var manualSessionSecret by remember { mutableStateOf("") }
8787
var timeRemaining by remember { mutableLongStateOf(0L) }
88+
var pastedUrl by remember { mutableStateOf("") }
8889

8990
val isPubkyRingInstalled = remember { bridge.isPubkyRingInstalled(context) }
9091
val recommendedMethod = remember { bridge.getRecommendedAuthMethod(context) }
9192

93+
// Handle scanned QR code
94+
LaunchedEffect(scannedQrCode) {
95+
scannedQrCode?.let { qrCode ->
96+
if (qrCode.contains("pubky://") || qrCode.contains("pubkyring://")) {
97+
try {
98+
val session = bridge.handleAuthUrl(qrCode)
99+
onSessionReceived(session)
100+
} catch (e: Exception) {
101+
errorMessage = e.message ?: "Failed to process QR code"
102+
}
103+
} else {
104+
pastedUrl = qrCode
105+
}
106+
}
107+
}
108+
92109
// Set initial tab based on availability
93110
LaunchedEffect(Unit) {
94111
if (!isPubkyRingInstalled) {
@@ -117,9 +134,9 @@ fun PubkyRingAuthScreen(
117134
}
118135

119136
val tabs = if (isPubkyRingInstalled) {
120-
listOf("Same Device", "QR Code", "Manual")
137+
listOf("Same Device", "Show QR", "Scan/Paste")
121138
} else {
122-
listOf("QR Code", "Manual")
139+
listOf("Show QR", "Scan/Paste")
123140
}
124141

125142
Scaffold(
@@ -194,17 +211,24 @@ fun PubkyRingAuthScreen(
194211
}
195212
},
196213
)
197-
2 -> ManualEntryTabContent(
198-
pubkey = manualPubkey,
199-
onPubkeyChange = { manualPubkey = it },
200-
sessionSecret = manualSessionSecret,
201-
onSessionSecretChange = { manualSessionSecret = it },
202-
onImport = {
203-
val session = bridge.importSession(
204-
pubkey = manualPubkey.trim(),
205-
sessionSecret = manualSessionSecret.trim(),
206-
)
207-
onSessionReceived(session)
214+
2 -> ScanPasteTabContent(
215+
pastedUrl = pastedUrl,
216+
onPastedUrlChange = { pastedUrl = it },
217+
onScanClick = onNavigateToScanner,
218+
onPasteFromClipboard = {
219+
clipboardManager.getText()?.text?.let { text ->
220+
pastedUrl = text
221+
}
222+
},
223+
onConnect = {
224+
scope.launch {
225+
try {
226+
val session = bridge.handleAuthUrl(pastedUrl.trim())
227+
onSessionReceived(session)
228+
} catch (e: Exception) {
229+
errorMessage = e.message ?: "Invalid Pubky Ring URL"
230+
}
231+
}
208232
},
209233
)
210234
}
@@ -425,12 +449,12 @@ private fun CrossDeviceTabContent(
425449
}
426450

427451
@Composable
428-
private fun ManualEntryTabContent(
429-
pubkey: String,
430-
onPubkeyChange: (String) -> Unit,
431-
sessionSecret: String,
432-
onSessionSecretChange: (String) -> Unit,
433-
onImport: () -> Unit,
452+
private fun ScanPasteTabContent(
453+
pastedUrl: String,
454+
onPastedUrlChange: (String) -> Unit,
455+
onScanClick: (() -> Unit)?,
456+
onPasteFromClipboard: () -> Unit,
457+
onConnect: () -> Unit,
434458
modifier: Modifier = Modifier,
435459
) {
436460
Column(
@@ -441,7 +465,7 @@ private fun ManualEntryTabContent(
441465
horizontalAlignment = Alignment.CenterHorizontally,
442466
) {
443467
Icon(
444-
imageVector = Icons.Default.Keyboard,
468+
imageVector = Icons.Default.QrCode,
445469
contentDescription = null,
446470
modifier = Modifier.size(60.dp),
447471
tint = Colors.Brand,
@@ -450,54 +474,71 @@ private fun ManualEntryTabContent(
450474
Spacer(modifier = Modifier.height(24.dp))
451475

452476
Text(
453-
text = "Manual Entry",
477+
text = "Scan or Paste",
454478
style = MaterialTheme.typography.headlineSmall,
455479
fontWeight = FontWeight.Bold,
456480
)
457481

458482
Spacer(modifier = Modifier.height(8.dp))
459483

460484
Text(
461-
text = "Enter your Pubky credentials manually if other methods aren't available.",
485+
text = "Scan a Pubky Ring QR code or paste the connection URL.",
462486
style = MaterialTheme.typography.bodyMedium,
463487
color = Colors.White64,
464488
textAlign = TextAlign.Center,
465489
)
466490

467491
Spacer(modifier = Modifier.height(32.dp))
468492

469-
OutlinedTextField(
470-
value = pubkey,
471-
onValueChange = onPubkeyChange,
472-
label = { Text("Public Key (z-base32)") },
473-
placeholder = { Text("e.g., z6mk...") },
474-
modifier = Modifier.fillMaxWidth(),
475-
singleLine = true,
476-
)
493+
if (onScanClick != null) {
494+
Button(
495+
onClick = onScanClick,
496+
modifier = Modifier.fillMaxWidth(),
497+
colors = ButtonDefaults.buttonColors(
498+
containerColor = MaterialTheme.colorScheme.secondary,
499+
),
500+
) {
501+
Icon(Icons.Default.QrCode, contentDescription = null)
502+
Spacer(modifier = Modifier.width(8.dp))
503+
Text("Scan QR Code")
504+
}
505+
506+
Spacer(modifier = Modifier.height(16.dp))
477507

478-
Spacer(modifier = Modifier.height(16.dp))
508+
Text(
509+
text = "or paste a URL",
510+
style = MaterialTheme.typography.bodySmall,
511+
color = Colors.White64,
512+
)
513+
514+
Spacer(modifier = Modifier.height(16.dp))
515+
}
479516

480517
OutlinedTextField(
481-
value = sessionSecret,
482-
onValueChange = onSessionSecretChange,
483-
label = { Text("Session Secret") },
484-
placeholder = { Text("Secret from Pubky-ring") },
518+
value = pastedUrl,
519+
onValueChange = onPastedUrlChange,
520+
label = { Text("Pubky Ring URL") },
521+
placeholder = { Text("pubky://... or pubkyring://...") },
485522
modifier = Modifier.fillMaxWidth(),
486523
singleLine = true,
487-
visualTransformation = PasswordVisualTransformation(),
524+
trailingIcon = {
525+
IconButton(onClick = onPasteFromClipboard) {
526+
Icon(Icons.Default.ContentCopy, contentDescription = "Paste from clipboard")
527+
}
528+
},
488529
)
489530

490531
Spacer(modifier = Modifier.height(32.dp))
491532

492533
Button(
493-
onClick = onImport,
534+
onClick = onConnect,
494535
modifier = Modifier.fillMaxWidth(),
495-
enabled = pubkey.isNotBlank() && sessionSecret.isNotBlank(),
536+
enabled = pastedUrl.isNotBlank(),
496537
colors = ButtonDefaults.buttonColors(
497538
containerColor = Colors.Brand,
498539
),
499540
) {
500-
Text("Import Session")
541+
Text("Connect")
501542
}
502543
}
503544
}

app/src/main/java/to/bitkit/ui/ContentView.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1655,6 +1655,7 @@ private fun NavGraphBuilder.paykit(navController: NavHostController) {
16551655
onNavigateToContactDiscovery = { navController.navigate(Routes.PaykitContactDiscovery) },
16561656
onNavigateToPrivateEndpoints = { navController.navigate(Routes.PaykitPrivateEndpoints) },
16571657
onNavigateToRotationSettings = { navController.navigate(Routes.PaykitRotationSettings) },
1658+
onNavigateToPubkyRingAuth = { navController.navigate(Routes.PubkyRingAuth) },
16581659
)
16591660
}
16601661
composableWithDefaultTransitions<Routes.PaykitContacts> {
@@ -1718,6 +1719,23 @@ private fun NavGraphBuilder.paykit(navController: NavHostController) {
17181719
onNavigateBack = { navController.popBackStack() }
17191720
)
17201721
}
1722+
composableWithDefaultTransitions<Routes.PubkyRingAuth> { backstackEntry ->
1723+
// Check for scanned QR code result
1724+
val scannedQrCode = backstackEntry.savedStateHandle.get<String>("scanned_qr_code")
1725+
1726+
to.bitkit.paykit.ui.PubkyRingAuthScreen(
1727+
onNavigateBack = { navController.popBackStack() },
1728+
onSessionReceived = { session ->
1729+
// Session is already cached in PubkyRingBridge.importSession()
1730+
navController.popBackStack()
1731+
},
1732+
onNavigateToScanner = {
1733+
backstackEntry.savedStateHandle[SCAN_REQUEST_KEY] = true
1734+
navController.navigate(Routes.QrScanner)
1735+
},
1736+
scannedQrCode = scannedQrCode,
1737+
)
1738+
}
17211739
}
17221740

17231741
@Stable
@@ -2069,4 +2087,7 @@ sealed interface Routes {
20692087

20702088
@Serializable
20712089
data object PaykitRotationSettings : Routes
2090+
2091+
@Serializable
2092+
data object PubkyRingAuth : Routes
20722093
}

0 commit comments

Comments
 (0)