Custom integration

Call the headless PaspoID API directly from Compose, an Activity, or a Fragment.

To use your own UI, call the SDK directly. Create a PaspoID from the host Activity and call authenticate from a coroutine. The method does not throw; every outcome, including cancellation and a missing Paspo ID application, is returned as a PaspoAuthResult.

Two constraints apply:

  • Only one authenticate may run at a time; a new call resets the previous one.
  • PaspoID holds an Activity reference and must remain in the UI layer. Do not store it in a ViewModel or a singleton; pass only the result to the ViewModel.

Compose

@Composable
fun LoginScreen() {
    val activity = LocalActivity.current as? ComponentActivity ?: return
    val paspoId = remember(activity) { PaspoID(activity) }
    val scope = rememberCoroutineScope()

    Button(
        onClick = {
            scope.launch {
                onPaspoResult(paspoId.authenticate(PaspoScope.PHONES, backend.requestSsoNonce()))
            }
        },
    ) {
        Text("Sign in with Paspo")
    }
}

remember(activity) retains a single PaspoID across recompositions; rememberCoroutineScope cancels the call if the user leaves the screen.

Activity

class LoginActivity : AppCompatActivity() {

    private val paspoId = PaspoID(this)

    private fun signInWithPaspo() = lifecycleScope.launch {
        onPaspoResult(paspoId.authenticate(PaspoScope.PHONES, backend.requestSsoNonce()))
    }
}

Fragment

class LoginFragment : Fragment(R.layout.fragment_login) {

    // lazy: the Activity is not attached when the Fragment is constructed
    private val paspoId by lazy { PaspoID(requireActivity() as ComponentActivity) }

    private fun signInWithPaspo() = viewLifecycleOwner.lifecycleScope.launch {
        onPaspoResult(paspoId.authenticate(PaspoScope.PHONES, backend.requestSsoNonce()))
    }
}

Launch on viewLifecycleOwner.lifecycleScope rather than the Fragment's own scope, so the callback does not run against a destroyed view.

Activity recreation

If the Activity is recreated while the Paspo ID screen is open (rotation, process death), the result is lost. Treat this as a cancellation and allow the user to retry; locking the orientation of the sign-in screen is the simplest mitigation.

Sign-in initiated from Paspo ID

A user may also start sign-in from within the Paspo ID app. In that case Paspo ID launches your application with the action paspo.id.ssoprovider.action.AUTHENTICATE (the constant PaspoID.ACTION_SIGN_IN). Support for this entry point is optional.

Declare an intent-filter on the sign-in Activity:

<activity
    android:name=".LoginActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="paspo.id.ssoprovider.action.AUTHENTICATE" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

and start the flow when the action is received:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    if (intent.action == PaspoID.ACTION_SIGN_IN) {
        signInWithPaspo()
    }
}

The intent carries no additional data; it is solely a signal that the user intends to sign in.

Checking availability

To adapt the UI before any interaction:

if (paspoId.checkInstallation()) {
    // Paspo ID is installed
}

This call is optional: authenticate performs the check and, when Paspo ID is not installed, opens the Play Store and returns NotInstalled.

paspoId.cleanup() clears the ephemeral keys. The SDK invokes it automatically on completion and on errors; call it manually only when abandoning a flow yourself, for example when the user leaves the sign-in screen.