mcpField
P
ping-orchestration-android-sdk

ping-orchestration-android-sdk

github.com/pingidentitySecurity146 installsUpdated 13w ago
Repository

About ping-orchestration-android-sdk

A skill for generating Ping Orchestration Android SDK authentication integrations or a complete Android project. It guides configuration for Journey, DaVinci, and OIDC Web flows and includes implementation details for callbacks and collectors.

Key features

  1. 01Scaffold complete Android projects
  2. 02Generate Kotlin integration files
  3. 03Configure Journey, DaVinci, and OIDC Web
  4. 04Render callbacks and collectors
  5. 05Validate OAuth and Gradle settings

Use cases

  1. 01Create a new Android authentication sample
  2. 02Add Ping SDK authentication to an existing app
  3. 03Look up SDK flow and callback guidance

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/pingidentity/ping-sdk-agent-skills --skill ping-orchestration-android-sdk

After installing, type/ping-orchestration-android-sdkin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

README.md
assets/
Theme.kt.template
references/
callback-reference.md
common-mistakes.md

Details

From the project README

Skill Parameters

When invoked with no arguments, run the wizard. With arguments, generate inline.

No-arg Invocation — Wizard Mode

Step W1 — Determine intent using AskUserQuestion:

"What would you like to do?"
Options:
  A) Scaffold a new Android project   — complete Gradle project with Ping SDK wired in
  B) Add to an existing project       — generate only the Kotlin files to drop in
  C) Browse the reference guide       — show the full SDK reference
  D) Something else                   — let me describe what I need
  • A/B → Step W2 to collect config, then generate
  • C → display the integration guide below. Stop.
  • D → follow-up free-text question, route accordingly.

Step W2 — Collect configuration (A and B only).

Ask all required parameters in a single AskUserQuestion. Show defaults where they exist. Do not generate until every required field has a value.

ParameterRequiredDefaultDescription
flowTypeYes—davinci · journey · oidc-web
clientIdYes—OAuth 2.0 Client ID
discoveryEndpointYes—Full .well-known/openid-configuration URL
redirectUriYescom.example.myapp:/oauth2redirectOAuth 2.0 redirect URI (must match manifest scheme)
scopesNoopenid profile emailSpace-separated OAuth 2.0 scopes
serverUrlJourney only—PingAM/AIC base URL (no trailing /)
realmJourney onlyalphaAuthentication realm
cookieNameJourney onlyiPlanetDirectoryProSSO cookie name
journeyNameJourney onlyLoginJourney tree name
callbackTierJourney/DaVinci—basic, standard, or full (see Section 11)
packageNameYes (A only)—Android package name, e.g. com.example.myapp
outputPathYes (A only)—Absolute path for project output

Validation rules:

  • redirectUri scheme must match manifestPlaceholders["appRedirectUriScheme"] in build.gradle.kts.
  • discoveryEndpoint must end with /.well-known/openid-configuration.
  • serverUrl (Journey) must start with https:// and have no trailing /.
  • scopes must include openid. If missing, prepend it and warn.

Step W3 — Confirm and generate. Summarise values in a short table, ask "Ready to generate — does this look right?", then proceed.

Step W4 — Post-generation checklist. Run through this before reporting done:

  1. Maven repository — settings.gradle.kts needs only google() + mavenCentral(). Do NOT add maven.forgerock.org (401) or maven.pingidentity.com. SDK 2.0.0+ is on Maven Central.

  2. gradle.properties — must exist at project root:

    android.useAndroidX=true
    android.enableJetifier=true
    org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
    kotlin.code.style=official
    

    Missing this = build fails with AndroidX classpath wall.

  3. gradlew wrapper — omit from scaffolded output; tell user to open in Android Studio first.

  4. ExposedDropdownMenuBox anchor — use Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), import androidx.compose.material3.MenuAnchorType. No-arg overload deprecated in Material3 1.3.x.

  5. Versions baseline — agp = "8.9.1", kotlin = "2.1.21", compileSdk = 36, targetSdk = 36. AGP 8.9.1 requires Kotlin 2.1.x (2.0.x → K2 crash FirIncompatibleClassExpressionChecker). androidx.browser:1.9.0 and androidx.core:1.17.0+ require AGP 8.9.1+ and compileSdk 36.

  6. minSdk floor — Journey: minSdk = 29. DaVinci/OIDC: minSdk = 28.

  7. Ping Identity branding — always apply. Read assets/Theme.kt.template, write to ui/theme/PingTheme.kt, substitute PLACEHOLDER_PACKAGE_NAME. Copy assets/ping_logo.png to app/src/main/res/drawable/.

  8. res/values/themes.xml parent — use Theme.AppCompat.Light.NoActionBar, NOT Theme.Material3.DayNight.NoActionBar. The Material3 XML theme requires the com.google.android.material library which is not in the dependency list. Theme.AppCompat.Light.NoActionBar is always available transitively via activity-compose. Using the wrong parent → AAPT resource linking failure at build time.

  9. App icon — never reference @mipmap/ic_launcher — scaffolded projects have no mipmap directories. In AndroidManifest.xml use @drawable/ping_logo (already copied in step 7) for both android:icon and android:roundIcon. Referencing a missing mipmap → AAPT resource linking failure identical to the theme error above.


Ping Orchestration Android SDK — Integration Guide

Flow Types

Journey (native) — Compose UI in-app, callbacks, targets PingOne AIC / PingAM. DaVinci — Like Journey but targets PingOne DaVinci; uses collectors, no serverUrl/realm/cookie. OIDC Web — Browser via Custom Tabs; wire redirect URI intent filter.

Module Map

Artifact (com.pingidentity.sdks:<name>)When to add
journeyJourney flows + all callback types
davinciDaVinci flows + all collector types
oidcStandalone OidcWebClient
external-idpSocial login (IdpCollector, SelectIdpCallback)
fidoFIDO2 / Passkeys
protectPingOne Protect risk signals
bindingDevice binding & signing
device-profileDevice metadata
oathTOTP/HOTP — artifact is oath, NOT mfa-oath
pushPush MFA — artifact is push, NOT mfa-push
recaptcha-enterprisereCAPTCHA Enterprise

1 — Gradle Dependencies

SDK 2.0.0+ is on Maven Central — no custom repo needed.

// settings.gradle.kts
dependencyResolutionManagement {
    repositories { google(); mavenCentral() }
}
// app/build.gradle.kts
android {
    compileSdk = 36
    defaultConfig {
        minSdk = 29
        targetSdk = 36
        manifestPlaceholders["appRedirectUriScheme"] = "com.example.myapp"
    }
    buildFeatures { buildConfig = true }
}

dependencies {
    implementation("com.pingidentity.sdks:journey:2.0.0")   // or :davinci:2.0.0
    implementation("com.pingidentity.sdks:fido:2.0.0")
    implementation("com.pingidentity.sdks:protect:2.0.0")
    implementation("com.pingidentity.sdks:binding:2.0.0")
    implementation("com.pingidentity.sdks:device-profile:2.0.0")
    implementation("com.pingidentity.sdks:external-idp:2.0.0")

    val composeBom = platform("androidx.compose:compose-bom:<bom_version>")
    implementation(composeBom)
    implementation("androidx.compose.material3:material3")
    implementation("androidx.activity:activity-compose")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose")
    implementation("androidx.navigation:navigation-compose")
}

gradle/libs.versions.toml baseline: agp = "8.9.1", kotlin = "2.1.21".

2 — AndroidManifest.xml — Redirect URI

<activity android:name=".MainActivity" ...>
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="${appRedirectUriScheme}" />
    </intent-filter>
</activity>

3 — Node Types (com.pingidentity.orchestrate)

sealed interface Node
// ContinueNode  — more input needed; .next() to advance
// SuccessNode   — authenticated; .session
// ErrorNode     — server error (recoverable); .message: String
// FailureNode   — unexpected error; .cause: Throwable (NON-OPTIONAL)

Node is not Equatable — use counter: Int in state to force recomposition.

4 — DaVinci Configuration

import com.pingidentity.davinci.DaVinci
import com.pingidentity.davinci.module.Oidc
import com.pingidentity.logger.Logger
import com.pingidentity.logger.STANDARD  // top-level extension — explicit import required

val daVinci = DaVinci {
    logger = Logger.STANDARD
    module(Oidc) {
        clientId          = BuildConfig.CLIENT_ID
        discoveryEndpoint = BuildConfig.DISCOVERY_ENDPOINT
        scopes            = mutableSetOf("openid", "profile", "email")
        redirectUri       = BuildConfig.REDIRECT_URI
    }
}

// Start: val node: Node = daVinci.start()          — suspend
// Advance: val next: Node = continueNode.next()    — suspend
// Session: val user = daVinci.user()               — suspend, null if none
// Logout: user?.logout()
// Token: user?.accessToken()  // Result<Token, OidcError>

5 — Journey Configuration

import com.pingidentity.journey.Journey
import com.pingidentity.journey.module.Oidc
import com.pingidentity.logger.Logger
import com.pingidentity.logger.STANDARD     // top-level extension
import com.pingidentity.journey.start       // top-level extension — NOT a Journey member
import com.pingidentity.journey.user        // top-level extension — NOT a Journey member

val journey = Journey {
    logger    = Logger.STANDARD
    serverUrl = BuildConfig.SERVER_URL
    realm     = BuildConfig.REALM
    cookie    = BuildConfig.COOKIE_NAME
    module(Oidc) {
        clientId          = BuildConfig.CLIENT_ID
        discoveryEndpoint = BuildConfig.DISCOVERY_ENDPOINT
        scopes            = mutableSetOf("openid", "profile", "email")
        redirectUri       = BuildConfig.REDIRECT_URI
    }
}

// Start:   val node: Node = journey.start(BuildConfig.JOURNEY_NAME)  — suspend
// Session: val user = journey.user()   — suspend, null if none
// Logout:  user?.logout()
// Token:   user?.token()  // Result<Token, OidcError>

All four symbols below are top-level extensions — unresolved if not imported:

SymbolImport
journey.start(name)import com.pingidentity.journey.start
journey.user()import com.pingidentity.journey.user
continueNode.callbacksimport com.pingidentity.journey.plugin.callbacks
Logger.STANDARDimport com.pingidentity.logger.STANDARD

6 — MVVM State & ViewModel

data class AuthState(val node: Node? = null, val counter: Int = 0)

class AuthViewModel : ViewModel() {
    val state = MutableStateFlow(AuthState())
    val loading = MutableStateFlow(false)

    fun start() { viewModelScope.launch {
        loading.value = true
        val node = journey.start(BuildConfig.JOURNEY_NAME)
        state.update { AuthState(node = node, counter = it.counter + 1) }
        loading.value = false
    }}

    fun next(continueNode: ContinueNode) { viewModelScope.launch {
        loading.value = true
        val node = continueNode.next()
        state.update { AuthState(node = node, counter = it.counter + 1) }
        loading.value = false
    }}

    fun refresh() { state.update { it.copy(counter = it.counter + 1) } }

    fun logout(onCompleted: () -> Unit) { viewModelScope.launch {
        journey.user()?.logout()
        state.value = AuthState()
        onCompleted()
    }}
}

7 — Top-Level Auth Screen

@Composable
fun AuthScreen(viewModel: AuthViewModel, onSuccess: () -> Unit) {
    BackHandler { viewModel.start() }  // restart rather than navigate back
    val state by viewModel.state.collectAsState()
    val loading by viewModel.loading.collectAsState()

    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        when (val node = state.node) {
            is ContinueNode -> CallbackNodeView(node, { viewModel.refresh() }) { viewModel.next(node) }
            is FailureNode  -> PingErrorCard(node.cause.message ?: "Unexpected error") { viewModel.start() }
            is ErrorNode    -> PingErrorCard(node.message) { viewModel.start() }
            is SuccessNode  -> LaunchedEffect(Unit) { onSuccess() }  // LaunchedEffect prevents recomposition loop
            null            -> LaunchedEffect(Unit) { viewModel.start() }
        }
        if (loading) PingLoadingOverlay("Signing in…")
    }
}

8 — DaVinci: Collector Rendering

@Composable
fun ContinueNodeView(node: ContinueNode, onNodeUpdated: () -> Unit, onNext: (ContinueNode) -> Unit) {
    var hasSubmittable = false
    node.collectors.forEach { collector ->
        when (collector) {
            is TextCollector     -> TextCollectorField(collector, onNodeUpdated)
            is PasswordCollector -> PasswordCollectorField(collector, onNodeUpdated)
            is LabelCollector    -> Text(collector.content, style = MaterialTheme.typography.bodyMedium)
            is SingleSelectCollector -> SingleSelectField(collector, onNodeUpdated)
            is MultiSelectCollector  -> MultiSelectField(collector, onNodeUpdated)
            is PhoneNumberCollector  -> PhoneNumberField(collector, onNodeUpdated)
            is SubmitCollector -> { hasSubmittable = true; SubmitButton(collector) { onNext(node) } }
            is FlowCollector   -> { hasSubmittable = true; FlowButton(collector) { onNext(node) } }
            is DeviceRegistrationCollector   -> { hasSubmittable = true; DeviceRegistrationView(collector) { onNext(node) } }
            is DeviceAuthenticationCollector -> { hasSubmittable = true; DeviceAuthView(collector) { onNext(node) } }
            is FidoRegistrationCollector     -> { hasSubmittable = true; FidoRegistrationView(collector) { onNext(node) } }
            is FidoAuthenticationCollector   -> { hasSubmittable = true; FidoAuthView(collector) { onNext(node) } }
            is ProtectCollector -> ProtectCollectorView(collector) { onNext(node) }
            is IdpCollector     -> IdpButton(collector) { onNext(node) }
        }
    }
    if (!hasSubmittable) { PingPrimaryButton("Next") { onNext(node) } }
}

Key collector properties:

CollectorKey propertiesInput
TextCollectorlabel, value, requiredcollector.value = text
PasswordCollectorlabel, valuecall collector.validate(pwd) first; collector.value = pwd
SubmitCollector / FlowCollectorlabelcall onNext
LabelCollectorcontentdisplay only
SingleSelectCollectorlabel, options: List<Option>, value: Stringcollector.value = option.value
MultiSelectCollectorlabel, options: List<Option>, value: List<String>collector.value = listOf(...)
PhoneNumberCollectorlabel, defaultCountryCode, countryCode, phoneNumberset both fields
DeviceRegistrationCollector / DeviceAuthenticationCollectordevices: List<Device>collector.value = device then onNext
ProtectCollector—suspend collector.collect() then onNext
FidoRegistrationCollector—suspend collector.register() then onNext
FidoAuthenticationCollector—suspend collector.authenticate() then onNext
IdpCollectorlabel, type, iconUrlsuspend collector.authorize(redirectUri)

9 — Journey: Callback Rendering

import com.pingidentity.journey.plugin.callbacks  // extension property — required

@Composable
fun CallbackNodeView(node: ContinueNode, onNodeUpdated: () -> Unit, onNext: () -> Unit) {
    var showNext = true
    node.callbacks.forEach { callback ->
        when (callback) {
            is NameCallback            -> NameField(callback, onNodeUpdated)
            is PasswordCallback        -> PasswordField(callback, onNodeUpdated)
            is ValidatedUsernameCallback -> ValidatedUsernameField(callback, onNodeUpdated)
            is ValidatedPasswordCallback -> ValidatedPasswordField(callback, onNodeUpdated)
            is TextInputCallback       -> TextInputField(callback, onNodeUpdated)
            is TextOutputCallback      -> Text(callback.message, style = MaterialTheme.typography.bodyMedium)
            is ChoiceCallback          -> ChoiceField(callback, onNodeUpdated)
            is ConfirmationCallback    -> { showNext = false; ConfirmationButtons(callback, onNext) }
            is TermsAndConditionsCallback -> TermsCheckbox(callback, onNodeUpdated)
            is KbaCreateCallback       -> KbaField(callback, onNodeUpdated)
            is BooleanAttributeInputCallback -> BooleanAttributeField(callback, onNodeUpdated)
            is StringAttributeInputCallback  -> StringAttributeField(callback, onNodeUpdated)
            is PollingWaitCallback     -> { showNext = false; PollingWaitView(callback, onNext) }
            is DeviceProfileCallback   -> { showNext = false; DeviceProfileView(callback, onNext) }
            is DeviceBindingCallback   -> { showNext = false; DeviceBindingView(callback, onNext) }
            is FidoRegistrationCallback   -> { showNext = false; FidoRegistrationView(callback, onNext) }
            is FidoAuthenticationCallback -> { showNext = false; FidoAuthView(callback, onNext) }
            is PingOneProtectInitializeCallback -> { showNext = false; ProtectInitView(callback, onNext) }
            is PingOneProtectEvaluationCallback -> { showNext = false; ProtectEvalView(callback, onNext) }
            is SelectIdpCallback -> { showNext = false; SelectIdpView(callback, onNext) }
        }
    }
    if (showNext) { PingPrimaryButton("Next", onClick = onNext) }
}

Callback reference (exact names, packages, property names, suspend methods — compile errors if guessed wrong): references/callback-reference.md

Key gotchas: ValidatedUsernameCallback → .username (not .value); TextInputCallback → .text; ChoiceCallback → .selectedIndex; TermsAndConditionsCallback → .accepted; DeviceProfileCallback.collect(), DeviceBindingCallback.bind(), PingOneProtectInitializeCallback.start().

Auto-advancing callbacks — wrap in LaunchedEffect(callback), set showNext = false: PollingWaitCallback, DeviceProfileCallback, DeviceBindingCallback, FidoRegistrationCallback, FidoAuthenticationCallback, PingOneProtectInitializeCallback, PingOneProtectEvaluationCallback.

10 — OIDC Web Flow

import com.pingidentity.oidc.OidcWebClient
import com.pingidentity.oidc.module.Oidc

val webClient = OidcWebClient {
    module(Oidc) {
        clientId = BuildConfig.CLIENT_ID
        discoveryEndpoint = BuildConfig.DISCOVERY_ENDPOINT
        scopes = mutableSetOf("openid", "profile", "email")
        redirectUri = BuildConfig.REDIRECT_URI
    }
}

// In ViewModel:
webClient.authorize()
    .onSuccess { user -> /* authenticated */ }
    .onFailure { error -> /* handle OidcError */ }

val user = webClient.user()  // null if no active session

authorize() is suspend. Launches Custom Tabs, waits for redirect, exchanges code for tokens.

11 — Collector/Callback Tiers

TierJourney additionsDaVinci additionsExtra modules
BasicNameCallback, PasswordCallback, TextOutputCallback, ChoiceCallbackTextCollector, PasswordCollector, SubmitCollector, LabelCollector, FlowCollectorNone
Standard+ validated callbacks, attribute callbacks, ConfirmationCallback, TermsAndConditionsCallback, KbaCreateCallback, PollingWaitCallback, all device/FIDO/Protect/SelectIdp callbacks+ SingleSelectCollector, MultiSelectCollector, PhoneNumberCollector, device/FIDO/Protect collectorsfido, protect, binding, device-profile, external-idp
Full+ IdpCallback, ReCaptchaEnterpriseCallback+ IdpCollector+ recaptcha-enterprise
FIDO / Protect / device binding?  No → Basic
  Yes → Social login or reCAPTCHA?  No → Standard  Yes → Full

12 — Common Mistakes

See references/common-mistakes.md — covers import errors, wrong callback property names, suspend method names, recomposition loops, Gradle/AGP version issues, AAPT errors, and branding component pitfalls.

13 — MFA Modules

Push (FCM) — artifact com.pingidentity.sdks:push:

val pushClient = PushClient { }
pushClient.addCredentialFromUri(uri)           // QR scan
pushClient.processNotification(remoteMessage.data)  // FCM
pushClient.approveNotification(notificationId)

OATH (TOTP/HOTP) — artifact com.pingidentity.sdks:oath:

val oathClient = OathClient { }
oathClient.addCredentialFromUri(otpauthUri)
val code = oathClient.generateCode(credentialId)

Protect (standalone):

Protect.config { isBehavioralDataCollection = true; envId = "api.pingone.com" }
Protect.init()

14 — ForgeRock → Ping Migration

LegacyPing SDK
org.forgerock:forgerock-authcom.pingidentity.sdks:journey
FRAuth.start()journey.start("Login")
FRUser.getCurrentUser()journey.user() / daVinci.user()
Node.getCallbacks()continueNode.callbacks

For a full guided migration, read the forgerock-to-ping-journey-migration skill.

15 — Ping Identity Visual Branding

Write assets/Theme.kt.template to ui/theme/PingTheme.kt (substitute PLACEHOLDER_PACKAGE_NAME). Copy assets/ping_logo.png → app/src/main/res/drawable/ping_logo.png.

Branded components: PingPrimaryButton, PingHeaderView, PingTextFieldBranded (name avoids clash with PingTextField color constant), PingSecureField, PingErrorMessage, PingErrorCard, PingLoadingOverlay (must be top sibling in Box(Modifier.fillMaxSize())).

Login screen: Box(fillMaxSize) wrapping a Column(verticalScroll) with PingHeaderView at the top, CallbackNodeView/CollectorNodeView below, and PingLoadingOverlay overlaid when loading. Apply PingRed tint to Checkbox/Switch.

Comments

Markdown supported