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.
| Parameter | Required | Default | Description |
|---|---|---|---|
flowType |
Yes | — | davinci · journey · oidc-web |
clientId |
Yes | — | OAuth 2.0 Client ID |
discoveryEndpoint |
Yes | — | Full .well-known/openid-configuration URL |
redirectUri |
Yes | com.example.myapp:/oauth2redirect |
OAuth 2.0 redirect URI (must match manifest scheme) |
scopes |
No | openid profile email |
Space-separated OAuth 2.0 scopes |
serverUrl |
Journey only | — | PingAM/AIC base URL (no trailing /) |
realm |
Journey only | alpha |
Authentication realm |
cookieName |
Journey only | iPlanetDirectoryPro |
SSO cookie name |
journeyName |
Journey only | Login |
Journey tree name |
callbackTier |
Journey/DaVinci | — | basic, standard, or full (see Section 11) |
packageName |
Yes (A only) | — | Android package name, e.g. com.example.myapp |
outputPath |
Yes (A only) | — | Absolute path for project output |
Validation rules:
redirectUrischeme must matchmanifestPlaceholders["appRedirectUriScheme"]inbuild.gradle.kts.discoveryEndpointmust end with/.well-known/openid-configuration.serverUrl(Journey) must start withhttps://and have no trailing/.scopesmust includeopenid. 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:
Maven repository —
settings.gradle.ktsneeds onlygoogle()+mavenCentral(). Do NOT addmaven.forgerock.org(401) ormaven.pingidentity.com. SDK 2.0.0+ is on Maven Central.gradle.properties— must exist at project root:android.useAndroidX=true android.enableJetifier=true org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 kotlin.code.style=officialMissing this = build fails with AndroidX classpath wall.
gradlewwrapper — omit from scaffolded output; tell user to open in Android Studio first.ExposedDropdownMenuBoxanchor — useModifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), importandroidx.compose.material3.MenuAnchorType. No-arg overload deprecated in Material3 1.3.x.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 crashFirIncompatibleClassExpressionChecker).androidx.browser:1.9.0andandroidx.core:1.17.0+require AGP 8.9.1+ andcompileSdk 36.minSdkfloor — Journey:minSdk = 29. DaVinci/OIDC:minSdk = 28.Ping Identity branding — always apply. Read
assets/Theme.kt.template, write toui/theme/PingTheme.kt, substitutePLACEHOLDER_PACKAGE_NAME. Copyassets/ping_logo.pngtoapp/src/main/res/drawable/.res/values/themes.xmlparent — useTheme.AppCompat.Light.NoActionBar, NOTTheme.Material3.DayNight.NoActionBar. The Material3 XML theme requires thecom.google.android.materiallibrary which is not in the dependency list.Theme.AppCompat.Light.NoActionBaris always available transitively viaactivity-compose. Using the wrong parent → AAPT resource linking failure at build time.App icon — never reference
@mipmap/ic_launcher— scaffolded projects have no mipmap directories. InAndroidManifest.xmluse@drawable/ping_logo(already copied in step 7) for bothandroid:iconandandroid: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 |
|---|---|
journey |
Journey flows + all callback types |
davinci |
DaVinci flows + all collector types |
oidc |
Standalone OidcWebClient |
external-idp |
Social login (IdpCollector, SelectIdpCallback) |
fido |
FIDO2 / Passkeys |
protect |
PingOne Protect risk signals |
binding |
Device binding & signing |
device-profile |
Device metadata |
oath |
TOTP/HOTP — artifact is oath, NOT mfa-oath |
push |
Push MFA — artifact is push, NOT mfa-push |
recaptcha-enterprise |
reCAPTCHA 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:
| Symbol | Import |
|---|---|
journey.start(name) |
import com.pingidentity.journey.start |
journey.user() |
import com.pingidentity.journey.user |
continueNode.callbacks |
import com.pingidentity.journey.plugin.callbacks |
Logger.STANDARD |
import 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:
| Collector | Key properties | Input |
|---|---|---|
TextCollector |
label, value, required |
collector.value = text |
PasswordCollector |
label, value |
call collector.validate(pwd) first; collector.value = pwd |
SubmitCollector / FlowCollector |
label |
call onNext |
LabelCollector |
content |
display only |
SingleSelectCollector |
label, options: List<Option>, value: String |
collector.value = option.value |
MultiSelectCollector |
label, options: List<Option>, value: List<String> |
collector.value = listOf(...) |
PhoneNumberCollector |
label, defaultCountryCode, countryCode, phoneNumber |
set both fields |
DeviceRegistrationCollector / DeviceAuthenticationCollector |
devices: 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 |
IdpCollector |
label, type, iconUrl |
suspend 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", }
}
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
| Tier | Journey additions | DaVinci additions | Extra modules |
|---|---|---|---|
| Basic | NameCallback, PasswordCallback, TextOutputCallback, ChoiceCallback |
TextCollector, PasswordCollector, SubmitCollector, LabelCollector, FlowCollector |
None |
| Standard | + validated callbacks, attribute callbacks, ConfirmationCallback, TermsAndConditionsCallback, KbaCreateCallback, PollingWaitCallback, all device/FIDO/Protect/SelectIdp callbacks |
+ SingleSelectCollector, MultiSelectCollector, PhoneNumberCollector, device/FIDO/Protect collectors |
fido, 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
| Legacy | Ping SDK |
|---|---|
org.forgerock:forgerock-auth |
com.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.