From 5d73874b613f8269ae00abcc7a2fd1f6fd48b46e Mon Sep 17 00:00:00 2001 From: Sy Bohy Date: Thu, 18 Jun 2026 12:57:00 -0700 Subject: [PATCH 1/2] feat: implement Android reference generator + regenerate pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the no-op stub with a working Kotlin→MDX generator that parses the Seam Android SDK's public sources (KDoc) into the reference pages — the Android counterpart of ios-reference/generate.ts. The generator owns these pages and the SDK's public KDoc is the source of truth. - parse data classes, sealed hierarchies, enums, and the SeamSDK API class (companion methods, StateFlow properties, suspend/non-suspend methods) - nesting-aware, comment/string-aware Kotlin scanning so KDoc that embeds `/* */` and code examples parses correctly - render frontmatter + Overview + per-member sections (signatures, KDoc prose, @param/@return/@throws), and a CardGroup index; KDoc [Ref] links → inline code - parses sources directly (no Gradle/Dokka build): Dokka 1.9 has no clean structured output, and the public surface is small and well-KDoc'd Regenerates all 8 pages (these now match generator output, replacing the hand-authored seed). Output is bounded by KDoc fidelity — curated tables/prose aren't reproduced and stale KDoc examples are emitted verbatim (fix upstream). Validates: eslint, tsc, mintlify validate, and broken-links all pass. Co-Authored-By: Claude Opus 4.8 --- mintlify-codegen/android-reference/README.md | 74 +- .../android-reference/generate.ts | 741 +++++++++++++++++- .../mobile-sdks/android/reference/index.mdx | 67 +- .../reference/seam-credential-error.mdx | 87 +- .../android/reference/seam-credential.mdx | 100 +-- .../android/reference/seam-error.mdx | 180 +++-- .../seam-required-user-interaction.mdx | 89 +-- .../android/reference/seam-sdk.mdx | 595 +++++++------- .../android/reference/seam-unlock-event.mdx | 85 +- .../android/reference/unlock-proximity.mdx | 53 +- 10 files changed, 1290 insertions(+), 781 deletions(-) diff --git a/mintlify-codegen/android-reference/README.md b/mintlify-codegen/android-reference/README.md index 11d927a9c..db35401ae 100644 --- a/mintlify-codegen/android-reference/README.md +++ b/mintlify-codegen/android-reference/README.md @@ -1,52 +1,46 @@ -# Android Reference Page Generator (SCAFFOLD) +# Android Reference Page Generator -The Android counterpart of [`../ios-reference/`](../ios-reference/README.md). When -complete, it will regenerate `mintlify-docs/mobile-sdks/android/reference/*.mdx` -automatically from the Android SDK's Dokka output. +The Android counterpart of [`../ios-reference/`](../ios-reference/README.md). It +regenerates `mintlify-docs/mobile-sdks/android/reference/*.mdx` from the Seam +Android SDK's public Kotlin sources. The generator **owns** these pages — it +replaces them wholesale on each run, so the SDK's public KDoc is the source of +truth (the same model as the iOS reference). -## Current Status: Not Implemented — Hand-Authored Pages in Place - -> The Android reference MDX pages are currently **hand-authored** (DOC-231, #1155) -> from the public Kotlin API in the `phone` repo -> (`seam-phone-android/seam-phone-sdk-android/core/src/main/java/co/seam/core/api/*.kt`). -> `generate.ts` is a **no-op stub**: it accepts `--archive` and exits 0 without -> writing, so the sync workflow stays a safe no-op and never overwrites those -> pages until the real generator lands. - -## Intended Pipeline +## Pipeline ``` -phone/seam-phone-android/ ← Kotlin SDK (Dokka 1.9.10 configured) - - ./gradlew dokkaHtmlMultiModule ← aggregates each module's dokkaHtmlPartial - ↓ -build/dokka/htmlMultiModule/ ← Dokka output +phone/seam-phone-android/seam-phone-sdk-android/ ← public Kotlin sources + KDoc tsx mintlify-codegen/android-reference/generate.ts \ - --archive build/dokka/htmlMultiModule + --source /path/to/seam-phone-sdk-android ↓ -mintlify-docs/mobile-sdks/android/reference/*.mdx +mintlify-docs/mobile-sdks/android/reference/*.mdx (+ index.mdx) ``` -The phone-side workflow is `seamapi/phone` -`.github/workflows/seam-phone-android-documentation.yml` — currently -`workflow_dispatch`-only. Enable its `push:` trigger once this generator works. -The docs-side automerge path (`mintlify-docs/mobile-sdks/**`) and the -`seam-ci-bot[bot]` author gate already cover Android, so no `automerge.yml` -change is needed. +The phone-side workflow `seamapi/phone` +`.github/workflows/seam-phone-android-documentation.yml` checks out both repos, +runs this generator against the checked-out Kotlin sources, and opens an +auto-merging PR authored by `seam-ci-bot[bot]`. The docs-side automerge path +(`mintlify-docs/mobile-sdks/**`) and the `seam-ci-bot[bot]` author gate already +cover Android, so no `automerge.yml` change is needed. -## Key Design Decision Before Implementing +## Why parse sources, not Dokka Unlike Swift-DocC (which emits clean per-symbol render JSON that -`ios-reference/generate.ts` parses), Dokka 1.9 has no first-class JSON output. -Pick one source to parse and design against a **real Dokka build's output**: - -- **`dokkaHtmlMultiModule`** — HTML; richest but hardest to parse cleanly. -- **`dokkaGfmMultiModule`** (GFM plugin) — Markdown; closest to MDX, likely the - least work. -- **Kotlin public sources** — parse `co.seam.core.api` declarations directly, - skipping Dokka entirely. - -Then mirror `../ios-reference/generate.ts` for the per-type page + index -rendering, and match the structure of the existing hand-authored pages so the -generator's first run is a clean diff rather than a rewrite. +`ios-reference/generate.ts` parses), Dokka 1.9 has no first-class structured +output — only HTML, which is fragile to parse. The Seam Android public API is a +small, well-KDoc'd surface, so the generator parses the `.kt` sources directly +(no Gradle/Dokka build needed in CI). The `PAGES` manifest in `generate.ts` maps +each public type to its source file and output slug — edit a row to add or +retarget a page. + +## What it produces vs. what it can't + +Each page gets: frontmatter, an `## Overview` (declaration + class KDoc), +and member sections (properties / cases / methods) with signatures, KDoc prose, +and `@param`/`@return`/`@throws` tables. KDoc `[Ref]` links become inline code. + +It is **bounded by the KDoc**: curated extras that aren't in the source (friendly +provider-name tables, conceptual sections, hand-written examples) are not +reproduced, and any stale KDoc (e.g. examples referencing renamed symbols) is +emitted verbatim — fix those upstream in the SDK's KDoc. diff --git a/mintlify-codegen/android-reference/generate.ts b/mintlify-codegen/android-reference/generate.ts index bd293e832..b865b081d 100644 --- a/mintlify-codegen/android-reference/generate.ts +++ b/mintlify-codegen/android-reference/generate.ts @@ -1,52 +1,729 @@ /* eslint-disable no-console */ /** - * Android Reference Page Generator — SCAFFOLD (not yet implemented) + * Android Reference Page Generator * - * The Android counterpart of `../ios-reference/generate.ts`. When complete it - * will transform Dokka output (from `./gradlew dokkaHtmlMultiModule` in the - * seamapi/phone Android SDK) into - * `mintlify-docs/mobile-sdks/android/reference/*.mdx`. + * Transforms the Seam Android SDK's public Kotlin sources (KDoc) into + * `mintlify-docs/mobile-sdks/android/reference/*.mdx` — the Android counterpart + * of `../ios-reference/generate.ts`. * - * STATUS: stub. It parses its CLI contract and exits 0 WITHOUT writing any - * files, so the sync workflow is a safe no-op until the real generator lands — - * the hand-authored Android reference pages (DOC-231) are never overwritten. + * Dokka 1.9 has no clean per-symbol JSON (unlike Swift-DocC), so this parses the + * public sources directly. The PAGES manifest below maps each documented public + * type to its source file and output slug; edit a row to add/retarget a page. + * The generator OWNS these pages — it replaces them wholesale on each run, so + * the public KDoc in the SDK is the source of truth. * - * To implement, see README.md (esp. the Dokka-output design decision) and - * mirror ../ios-reference/generate.ts. - * - * Usage (once implemented): - * tsx mintlify-codegen/android-reference/generate.ts --archive /path/to/dokka/htmlMultiModule - * DOKKA_ARCHIVE=/path/to/dokka/htmlMultiModule tsx mintlify-codegen/android-reference/generate.ts + * Usage: + * tsx mintlify-codegen/android-reference/generate.ts --source /path/to/seam-phone-sdk-android + * ANDROID_SDK_SOURCE=/path/to/seam-phone-sdk-android tsx .../generate.ts */ +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' import { argv, env, exit } from 'node:process' +import { fileURLToPath } from 'node:url' -function resolveArchivePath(): string | undefined { - const flagIndex = argv.indexOf('--archive') - if (flagIndex !== -1 && argv[flagIndex + 1] != null) { - return argv[flagIndex + 1] +// --------------------------------------------------------------------------- +// Manifest — public type -> source file (relative to --source root) + slug +// --------------------------------------------------------------------------- + +interface PageSpec { + type: string + sourceFile: string + slug: string +} + +const PAGES: PageSpec[] = [ + { + type: 'SeamSDK', + sourceFile: 'core/src/main/java/co/seam/core/api/SeamSDK.kt', + slug: 'seam-sdk', + }, + { + type: 'SeamCredential', + sourceFile: 'core/src/main/java/co/seam/core/api/SeamCredential.kt', + slug: 'seam-credential', + }, + { + type: 'SeamError', + sourceFile: 'core/src/main/java/co/seam/core/sdkerrors/SeamError.kt', + slug: 'seam-error', + }, + { + type: 'SeamCredentialError', + sourceFile: 'core/src/main/java/co/seam/core/sdkerrors/SeamError.kt', + slug: 'seam-credential-error', + }, + { + type: 'SeamRequiredUserInteraction', + sourceFile: 'core/src/main/java/co/seam/core/sdkerrors/SeamError.kt', + slug: 'seam-required-user-interaction', + }, + { + type: 'SeamUnlockEvent', + sourceFile: 'core/src/main/java/co/seam/core/events/SeamUnlockEvent.kt', + slug: 'seam-unlock-event', + }, + { + type: 'UnlockProximity', + sourceFile: 'core/src/main/java/co/seam/core/api/UnlockProximity.kt', + slug: 'unlock-proximity', + }, +] + +const OUTPUT_DIR_REL = 'mobile-sdks/android/reference' + +// --------------------------------------------------------------------------- +// Parsed model +// --------------------------------------------------------------------------- + +type TypeKind = + | 'data class' + | 'sealed class' + | 'enum class' + | 'class' + | 'interface' + | 'object' + +interface Kdoc { + body: string + params: Array<{ name: string; desc: string }> + returns: string | null + throwsList: Array<{ type: string; desc: string }> +} + +interface Member { + name: string + signature: string + doc: Kdoc +} + +interface MemberGroup { + title: string + members: Member[] +} + +interface ParsedType { + name: string + kind: TypeKind + declaration: string + doc: Kdoc + groups: MemberGroup[] +} + +// --------------------------------------------------------------------------- +// KDoc parsing +// --------------------------------------------------------------------------- + +/** Strip the comment framing and leading asterisks from each KDoc line. */ +function stripKdocFraming(raw: string): string[] { + return raw + .replace(/^\s*\/\*\*/, '') + .replace(/\*\/\s*$/, '') + .split('\n') + .map((line) => line.replace(/^\s*\* ?/, '').replace(/^\s*$/, '')) +} + +/** Convert KDoc `[Ref]` links to inline code, but never inside fenced code. */ +function convertRefs(body: string): string { + const segments = body.split(/(```[\s\S]*?```)/g) + return segments + .map((seg) => { + if (seg.startsWith('```')) return seg + // [Symbol] / [Symbol.member] not followed by ( → inline code. + return seg.replace(/\[([A-Za-z0-9_.]+)\](?!\()/g, '`$1`') + }) + .join('') +} + +const TAG_RE = /^@(param|return|returns|throws|see|property)\b\s*(.*)$/ + +function parseKdoc(raw: string | null): Kdoc { + const empty: Kdoc = { body: '', params: [], returns: null, throwsList: [] } + if (raw == null) return empty + + const lines = stripKdocFraming(raw) + const bodyLines: string[] = [] + const params: Array<{ name: string; desc: string }> = [] + const throwsList: Array<{ type: string; desc: string }> = [] + let returns: string | null = null + + type Cursor = + | { kind: 'body' } + | { kind: 'param'; idx: number } + | { kind: 'throws'; idx: number } + | { kind: 'returns' } + | { kind: 'ignore' } + let cursor: Cursor = { kind: 'body' } + let inFence = false + + for (const line of lines) { + if (line.trim().startsWith('```')) inFence = !inFence + + const tag = inFence ? null : line.match(TAG_RE) + if (tag != null) { + const kind = tag[1] + const rest = (tag[2] ?? '').trim() + if (kind === 'param' || kind === 'property') { + const sp = rest.indexOf(' ') + const name = sp === -1 ? rest : rest.slice(0, sp) + const desc = sp === -1 ? '' : rest.slice(sp + 1).trim() + params.push({ name, desc }) + cursor = { kind: 'param', idx: params.length - 1 } + } else if (kind === 'return' || kind === 'returns') { + returns = rest + cursor = { kind: 'returns' } + } else if (kind === 'throws') { + const sp = rest.indexOf(' ') + const type = sp === -1 ? rest : rest.slice(0, sp) + const desc = sp === -1 ? '' : rest.slice(sp + 1).trim() + throwsList.push({ type, desc }) + cursor = { kind: 'throws', idx: throwsList.length - 1 } + } else { + cursor = { kind: 'ignore' } // @see — dropped + } + continue + } + + // Continuation of whatever tag/body we are in. + if (cursor.kind === 'body') { + bodyLines.push(line) + } else if (cursor.kind === 'param') { + const p = params[cursor.idx] + if (p != null && line.trim() !== '') + {p.desc = `${p.desc} ${line.trim()}`.trim()} + } else if (cursor.kind === 'throws') { + const t = throwsList[cursor.idx] + if (t != null && line.trim() !== '') + {t.desc = `${t.desc} ${line.trim()}`.trim()} + } else if (cursor.kind === 'returns') { + if (line.trim() !== '') returns = `${returns} ${line.trim()}`.trim() + } } - return env['DOKKA_ARCHIVE'] + + const body = convertRefs(bodyLines.join('\n')) + .replace(/\n{3,}/g, '\n\n') + .trim() + return { + body, + params: params.map((p) => ({ name: p.name, desc: convertRefs(p.desc) })), + returns: returns != null ? convertRefs(returns) : null, + throwsList: throwsList.map((t) => ({ + type: t.type, + desc: convertRefs(t.desc), + })), + } +} + +// --------------------------------------------------------------------------- +// Kotlin source slicing +// --------------------------------------------------------------------------- + +/** Find the KDoc block ending immediately before `index` (skipping annotations). */ +function kdocEndingBefore(text: string, index: number): string | null { + // The type's KDoc is the last full KDoc block ending at/before `index`, with + // only annotations/whitespace between it and the declaration. + const candidate = findKdocBlocks(text.slice(0, index)).at(-1) + if (candidate == null) return null + const between = text.slice(candidate.end, index) + if (!/^[\s]*(@[\w()]+[ \t]*\n?)*[\s]*$/.test(between)) return null + return candidate.raw +} + +/** End index (exclusive) of a (possibly nested) block comment starting at `i`. */ +function endOfBlockComment(text: string, i: number): number { + let depth = 0 + let j = i + while (j < text.length) { + if (text[j] === '/' && text[j + 1] === '*') { + depth++ + j += 2 + } else if (text[j] === '*' && text[j + 1] === '/') { + depth-- + j += 2 + if (depth === 0) return j + } else j++ + } + return text.length +} + +/** End index (exclusive) of a string literal (handles `"""` and `"`) at `i`. */ +function endOfString(text: string, i: number): number { + if (text.slice(i, i + 3) === '"""') { + const e = text.indexOf('"""', i + 3) + return e === -1 ? text.length : e + 3 + } + let j = i + 1 + while (j < text.length) { + if (text[j] === '\\') j += 2 + else if (text[j] === '"') return j + 1 + else j++ + } + return text.length +} + +/** If `i` starts a comment or string, return the index just past it; else null. */ +function skipNonCode(text: string, i: number): number | null { + if (text[i] === '/' && text[i + 1] === '*') return endOfBlockComment(text, i) + if (text[i] === '/' && text[i + 1] === '/') { + const nl = text.indexOf('\n', i) + return nl === -1 ? text.length : nl + } + if (text[i] === '"') return endOfString(text, i) + return null } -function main(): void { - const archivePath = resolveArchivePath() +/** Match the closing delimiter for the opener at `open`, skipping comments/strings. */ +function matchDelimiter(text: string, open: number): number { + const opener = text[open] + const closer = opener === '(' ? ')' : opener === '{' ? '}' : '>' + let depth = 0 + let i = open + while (i < text.length) { + if (i !== open) { + const skip = skipNonCode(text, i) + if (skip != null) { + i = skip + continue + } + } + const c = text[i] + if (c === opener) depth++ + else if (c === closer) { + depth-- + if (depth === 0) return i + } + i++ + } + return -1 +} + +/** Find every top-level KDoc block, nesting-aware. */ +function findKdocBlocks( + text: string, +): Array<{ start: number; end: number; raw: string }> { + const blocks: Array<{ start: number; end: number; raw: string }> = [] + let i = 0 + while (i < text.length) { + if (text[i] === '/' && text[i + 1] === '*' && text[i + 2] === '*') { + const end = endOfBlockComment(text, i) + blocks.push({ start: i, end, raw: text.slice(i, end) }) + i = end + } else i++ + } + return blocks +} + +/** The first full KDoc block in `text`, or null. */ +function firstKdocBlock(text: string): string | null { + return findKdocBlocks(text)[0]?.raw ?? null +} - console.warn( - '[android-reference] Generator not yet implemented — this is a scaffold.', +interface TypeSlice { + kind: TypeKind + declaration: string + doc: Kdoc + ctorParamsRaw: string | null + bodyRaw: string | null +} + +function sliceType(text: string, typeName: string): TypeSlice { + const re = new RegExp( + `((?:public |private |internal |abstract |open |sealed |data |enum )*)\\b(class|interface|object)\\s+${typeName}\\b`, ) - console.warn( - '[android-reference] No pages written; hand-authored Android reference pages are preserved.', + const m = re.exec(text) + if (m == null || m.index == null) { + throw new Error(`Type "${typeName}" not found in source`) + } + const modifiers = (m[1] ?? '').trim() + const keyword = m[2] ?? 'class' + const kind: TypeKind = modifiers.includes('data') + ? 'data class' + : modifiers.includes('sealed') + ? 'sealed class' + : modifiers.includes('enum') + ? 'enum class' + : (keyword as TypeKind) + + const doc = parseKdoc(kdocEndingBefore(text, m.index)) + + // Walk from the end of the matched declaration header to find primary-ctor + // parens (if any) and/or the body `{ ... }`. + let i = m.index + m[0].length + // Skip `private/internal/protected constructor`. + const ctorKw = /^\s*(?:private|internal|protected)?\s*constructor\b/.exec( + text.slice(i), ) - if (archivePath != null) { - console.warn(`[android-reference] Received --archive: ${archivePath}`) + if (ctorKw != null) i += ctorKw[0].length + + let ctorParamsRaw: string | null = null + // Next significant char: `(` => primary ctor. + const afterWs = /^\s*/.exec(text.slice(i))?.[0].length ?? 0 + if (text[i + afterWs] === '(') { + const open = i + afterWs + const close = matchDelimiter(text, open) + ctorParamsRaw = text.slice(open + 1, close) + i = close + 1 } - console.warn( - '[android-reference] See android-reference/README.md and ios-reference/generate.ts to implement.', + + // Header runs until the body `{` or end of declaration. + const braceRel = text.slice(i).indexOf('{') + // Stop the header at a newline that is clearly past the declaration when no body. + const headerEnd = braceRel === -1 ? text.length : i + braceRel + const headerTail = text.slice(i, headerEnd).replace(/\s+/g, ' ').trim() + + let bodyRaw: string | null = null + if (braceRel !== -1) { + const open = i + braceRel + const close = matchDelimiter(text, open) + bodyRaw = text.slice(open + 1, close) + } + + // Build a clean declaration line (without embedded property KDoc). + let declaration: string + if (kind === 'data class' && ctorParamsRaw != null) { + const props = parseConstructorProps(ctorParamsRaw) + const body = props.map((p) => ` ${p.signature}`).join(',\n') + declaration = + `${modifiers} ${keyword} ${typeName}(\n${body}\n)`.trim() + } else { + const ctor = + ctorParamsRaw != null + ? `(${ctorParamsRaw.replace(/\s+/g, ' ').trim()})` + : '' + declaration = + `${modifiers} ${keyword} ${typeName}${ctor}${headerTail ? ` ${headerTail}` : ''}`.trim() + } + + return { kind, declaration, doc, ctorParamsRaw, bodyRaw } +} + +// --------------------------------------------------------------------------- +// Member extraction +// --------------------------------------------------------------------------- + +/** Split on depth-0 commas, respecting () <> {} and skipping comments/strings. */ +function splitTopLevel(text: string): string[] { + const out: string[] = [] + let depth = 0 + let start = 0 + let i = 0 + while (i < text.length) { + const skip = skipNonCode(text, i) + if (skip != null) { + i = skip + continue + } + const c = text[i] + if (c === '(' || c === '<' || c === '{') depth++ + else if (c === ')' || c === '>' || c === '}') depth-- + else if (c === ',' && depth === 0) { + out.push(text.slice(start, i)) + start = i + 1 + } + i++ + } + if (start < text.length) out.push(text.slice(start)) + return out +} + +/** Parse `val name: Type [= default]` segments (with optional leading KDoc). */ +function parseConstructorProps(raw: string): Member[] { + return splitTopLevel(raw) + .map((seg) => { + const kdocRaw = firstKdocBlock(seg) + const code = (kdocRaw != null ? seg.replace(kdocRaw, '') : seg).trim() + const decl = code.match(/\b(val|var)\s+(\w+)\s*:\s*([^=]+?)(?:\s*=.*)?$/s) + if (decl == null) return null + const keyword = decl[1] + const name = decl[2] ?? '' + const type = (decl[3] ?? '').replace(/\s+/g, ' ').trim() + return { + name, + signature: `${keyword} ${name}: ${type}`, + doc: parseKdoc(kdocRaw), + } + }) + .filter((m): m is Member => m != null) +} + +/** Collect KDoc'd members from a `{ ... }` body, given the parent kind. */ +function extractBodyMembers(body: string, parentKind: TypeKind): MemberGroup[] { + const funcs: Member[] = [] + const props: Member[] = [] + const cases: Member[] = [] + const companion: Member[] = [] + + const collectInto = (text: string, isCompanion: boolean): void => { + for (const block of findKdocBlocks(text)) { + const docRaw = block.raw + const after = text.slice(block.end) + // The declaration is the first non-blank, non-annotation code line(s). + const code = after.replace(/^(\s*@[\w()]+\s*)*/, '').replace(/^\s+/, '') + const doc = parseKdoc(docRaw) + + const fn = code.match( + /^(?:public |private |internal |override )*(suspend\s+)?fun\s+(\w+)\s*\(/, + ) + const nested = code.match( + /^(?:public |private |internal )*(data\s+)?(class|object)\s+(\w+)/, + ) + const prop = code.match( + /^(?:public |private |internal )*(val|var)\s+(\w+)/, + ) + const enumConst = code.match(/^([A-Z][A-Z0-9_]*)\s*[,;(]/) + + if (fn != null) { + const isSuspend = fn[1] != null + const name = fn[2] ?? '' + const sig = extractFunctionSignature(code) + if (isCompanion) companion.push({ name, signature: sig, doc }) + else + {funcs.push({ + name: `${isSuspend ? 'suspend ' : ''}${name}`, + signature: sig, + doc, + })} + } else if (nested != null) { + const name = nested[3] ?? '' + cases.push({ name, signature: extractNestedSignature(code), doc }) + } else if (parentKind === 'enum class' && enumConst != null) { + cases.push({ name: enumConst[1] ?? '', signature: '', doc }) + } else if (prop != null) { + const name = prop[2] ?? '' + props.push({ name, signature: extractPropertySignature(code), doc }) + } + } + } + + // Pull out a companion object body, collect it separately, then the rest. + const compMatch = body.match(/companion\s+object\s*\{/) + if (compMatch != null && compMatch.index != null) { + const open = body.indexOf('{', compMatch.index) + const close = matchDelimiter(body, open) + collectInto(body.slice(open + 1, close), true) + collectInto(body.slice(0, compMatch.index) + body.slice(close + 1), false) + } else { + collectInto(body, false) + } + + const groups: MemberGroup[] = [] + if (companion.length > 0) + {groups.push({ title: 'Companion object methods', members: companion })} + if (props.length > 0) groups.push({ title: 'Properties', members: props }) + if (cases.length > 0) + {groups.push({ + title: parentKind === 'enum class' ? 'Values' : 'Cases', + members: cases, + })} + if (funcs.length > 0) groups.push({ title: 'Methods', members: funcs }) + return groups +} + +/** `fun name(...): Ret` — declaration up to the body `{` or `=`. */ +function extractFunctionSignature(code: string): string { + const open = code.indexOf('(') + const close = matchDelimiter(code, open) + if (open === -1 || close === -1) return code.split('\n')[0]?.trim() ?? code + const head = code.slice(0, open).replace(/\s+/g, ' ').trim() + const params = code + .slice(open + 1, close) + .replace(/\s+/g, ' ') + .trim() + let tail = code.slice(close + 1) + const stop = tail.search(/[{=]/) + tail = (stop === -1 ? tail : tail.slice(0, stop)).replace(/\s+/g, ' ').trim() + return `${head}(${params})${tail ? ` ${tail}` : ''}`.trim() +} + +function extractNestedSignature(code: string): string { + const stop = code.search(/[{]/) + return (stop === -1 ? (code.split('\n')[0] ?? code) : code.slice(0, stop)) + .replace(/\s+/g, ' ') + .trim() +} + +function extractPropertySignature(code: string): string { + const line = code.split('\n')[0]?.trim() ?? code + const typed = line.match(/^((?:val|var)\s+\w+\s*:\s*[^=]+?)(?:\s*=.*)?$/) + if (typed != null) return (typed[1] ?? line).trim() + // No explicit type (e.g. `val x = internal.x`) → just `val name`. + const m = line.match(/^((?:val|var)\s+\w+)/) + return (m?.[1] ?? line).trim() +} + +// --------------------------------------------------------------------------- +// Parse a whole type +// --------------------------------------------------------------------------- + +function parseType(text: string, typeName: string): ParsedType { + const slice = sliceType(text, typeName) + let groups: MemberGroup[] = [] + if (slice.kind === 'data class' && slice.ctorParamsRaw != null) { + const props = parseConstructorProps(slice.ctorParamsRaw) + if (props.length > 0) groups = [{ title: 'Properties', members: props }] + } else if (slice.bodyRaw != null) { + groups = extractBodyMembers(slice.bodyRaw, slice.kind) + } + return { + name: typeName, + kind: slice.kind, + declaration: slice.declaration, + doc: slice.doc, + groups, + } +} + +// --------------------------------------------------------------------------- +// MDX rendering +// --------------------------------------------------------------------------- + +const AUTOGEN_NOTE = + '> Auto-generated from the Seam Android SDK Kotlin sources. Do not edit by hand — see `mintlify-codegen/android-reference/`.' + +function firstSentence(body: string): string { + const stripped = body + .replace(/```[\s\S]*?```/g, '') + .replace(/[`*[\]]/g, '') + .replace(/\s+/g, ' ') + .trim() + const m = stripped.match(/^(.*?[.!?])(\s|$)/) + return (m?.[1] ?? stripped).trim() +} + +function yamlEscape(s: string): string { + return s.replace(/'/g, "''") +} + +function renderParamsTable( + params: Array<{ name: string; desc: string }>, +): string[] { + if (params.length === 0) return [] + const rows = params.map((p) => `| \`${p.name}\` | ${p.desc || '—'} |`) + return [ + '', + '**Parameters**', + '', + '| Parameter | Description |', + '| --- | --- |', + ...rows, + ] +} + +function renderMember(m: Member): string[] { + const lines: string[] = [`### \`${m.name}\``, ''] + if (m.doc.body) lines.push(m.doc.body, '') + if (m.signature) lines.push('```kotlin', m.signature, '```') + lines.push(...renderParamsTable(m.doc.params)) + if (m.doc.returns) lines.push('', `**Returns** — ${m.doc.returns}`) + if (m.doc.throwsList.length > 0) { + lines.push('', '**Throws**', '') + for (const t of m.doc.throwsList) + {lines.push(`- \`${t.type}\`${t.desc ? ` — ${t.desc}` : ''}`)} + } + lines.push('') + return lines +} + +function renderTypePage(t: ParsedType): string { + const lines: string[] = [ + '---', + `title: '${yamlEscape(t.name)}'`, + `description: '${yamlEscape(firstSentence(t.doc.body))}'`, + '---', + '', + AUTOGEN_NOTE, + '', + '## Overview', + '', + '```kotlin', + t.declaration, + '```', + ] + if (t.doc.body) lines.push('', t.doc.body) + + for (const group of t.groups) { + lines.push('', '---', '', `## ${group.title}`, '') + if (group.title === 'Properties' && t.kind === 'data class') { + lines.push('| Property | Type | Description |', '| --- | --- | --- |') + for (const m of group.members) { + const type = m.signature.replace(/^(?:val|var)\s+\w+:\s*/, '') + lines.push( + `| \`${m.name}\` | \`${type}\` | ${m.doc.body.replace(/\n+/g, ' ') || '—'} |`, + ) + } + lines.push('') + } else { + for (const m of group.members) lines.push(...renderMember(m)) + } + } + return lines + .join('\n') + .replace(/\n{3,}/g, '\n\n') + .replace(/\n*$/, '\n') +} + +function renderIndexPage(types: ParsedType[]): string { + const cards = types + .map((t) => { + const slug = PAGES.find((p) => p.type === t.name)?.slug ?? '' + const desc = firstSentence(t.doc.body) || `${t.kind} reference` + return [ + ` `, + ` ${desc}`, + ' ', + ].join('\n') + }) + .join('\n') + return [ + '---', + `title: 'Android SDK API Reference'`, + `description: 'Public API surface of the Seam Android SDK.'`, + '---', + '', + AUTOGEN_NOTE, + '', + '', + cards, + '', + '', + ].join('\n') +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +function resolveSourceRoot(): string { + const flagIndex = argv.indexOf('--source') + if (flagIndex !== -1 && argv[flagIndex + 1] != null) { + return argv[flagIndex + 1] as string + } + const fromEnv = env['ANDROID_SDK_SOURCE'] + if (fromEnv != null) return fromEnv + console.error( + 'ERROR: provide the SDK source root via --source or ANDROID_SDK_SOURCE.', ) + exit(1) +} + +async function main(): Promise { + const sourceRoot = resolveSourceRoot() + const __dirname = dirname(fileURLToPath(import.meta.url)) + const outDir = join(__dirname, '..', '..', 'mintlify-docs', OUTPUT_DIR_REL) + await mkdir(outDir, { recursive: true }) - // Intentionally write nothing and succeed, so the sync workflow no-ops safely. - exit(0) + const parsed: ParsedType[] = [] + for (const page of PAGES) { + const text = await readFile(join(sourceRoot, page.sourceFile), 'utf-8') + const type = parseType(text, page.type) + parsed.push(type) + await writeFile(join(outDir, `${page.slug}.mdx`), renderTypePage(type)) + console.log( + `✓ ${page.slug}.mdx (${type.kind}, ${type.groups.reduce((n, g) => n + g.members.length, 0)} members)`, + ) + } + await writeFile(join(outDir, 'index.mdx'), renderIndexPage(parsed)) + console.log(`✓ index.mdx (${parsed.length} types)`) } -main() +await main() diff --git a/mintlify-docs/mobile-sdks/android/reference/index.mdx b/mintlify-docs/mobile-sdks/android/reference/index.mdx index efe4259a2..653011f5c 100644 --- a/mintlify-docs/mobile-sdks/android/reference/index.mdx +++ b/mintlify-docs/mobile-sdks/android/reference/index.mdx @@ -1,65 +1,30 @@ --- title: 'Android SDK API Reference' -description: 'Complete API reference for the Seam Android SDK — classes, data classes, enumerations, and sealed classes.' -sidebarTitle: 'Overview' +description: 'Public API surface of the Seam Android SDK.' --- -> **Interim hand-authored reference.** This page is authored directly from the Seam Android SDK public Kotlin sources. The long-term plan is to generate these pages from KDoc via Dokka. - -The Seam Android SDK exposes a reactive, coroutine-friendly API built around `StateFlow` properties and suspend functions. Use `SeamSDK` as the single entry point for all SDK operations. - -## Quick Start - -```kotlin -// 1. Initialize at app launch (suspend — call from a coroutine) -SeamSDK.initialize(context, "seam_cst_...") - -// 2. Activate to begin credential sync -SeamSDK.getInstance().activate() - -// 3. Observe credentials -SeamSDK.getInstance().credentials.collect { credentials -> - // Update your UI -} -``` - -## Classes +> Auto-generated from the Seam Android SDK Kotlin sources. Do not edit by hand — see `mintlify-codegen/android-reference/`. - - The main entry point for the Seam Android SDK. Manages SDK lifecycle, credential synchronization, unlock operations, and StateFlow-based status updates. + + This is the main entry point for the Seam Android SDK. - - -## Data Classes - - - - An immutable snapshot of a single access credential — its name, location, expiry, supported unlock proximities, and any active errors. + + A Seam credential object. - - -## Enumerations - - - - The required physical proximity for an unlock attempt: `TOUCH`, `NEARBY`, or `REMOTE`. + + Base class for all Seam errors. - - -## Sealed Classes - - - - Events emitted to the `unlockStatus` StateFlow as an unlock operation progresses from scanning through access granted, timeout, or reader error. + + A credential error is an error that is related to a credential. - - Errors thrown by `SeamSDK` methods for initialization, activation, credential, and network failures. + + A user interaction that is required to unlock a credential. - - Credential-specific errors that appear in `SeamCredential.errors` or are thrown wrapped in `SeamError.CredentialErrors`. + + Events related to the co.seam.core.api.SeamSDK.unlock operation. - - Actions the user must take to resolve a `SeamCredentialError.UserInteractionRequired` error — OTP authorization, enabling internet or Bluetooth, or granting permissions. + + Represents the proximity at which a credential used to unlock a door. diff --git a/mintlify-docs/mobile-sdks/android/reference/seam-credential-error.mdx b/mintlify-docs/mobile-sdks/android/reference/seam-credential-error.mdx index e7f1295cb..d9db498a9 100644 --- a/mintlify-docs/mobile-sdks/android/reference/seam-credential-error.mdx +++ b/mintlify-docs/mobile-sdks/android/reference/seam-credential-error.mdx @@ -1,79 +1,62 @@ --- title: 'SeamCredentialError' -description: 'Credential-specific errors that appear in SeamCredential.errors or are thrown wrapped in SeamError.CredentialErrors.' +description: 'A credential error is an error that is related to a credential.' --- -> **Interim hand-authored reference.** This page is authored directly from the Seam Android SDK public Kotlin sources. See the [reference overview](/mobile-sdks/android/reference/index) for context. +> Auto-generated from the Seam Android SDK Kotlin sources. Do not edit by hand — see `mintlify-codegen/android-reference/`. ## Overview -`SeamCredentialError` is a `sealed class` that extends `SeamError`. It represents errors specific to a single credential. These errors appear in two places: +```kotlin +sealed class SeamCredentialError : SeamError() +``` + +A credential error is an error that is related to a credential. When a credential error +occurs, it means that the credential is not valid or has some issues. It can be thrown when +trying to unlock with a credential, or they can come in the `SeamCredential.errors` list. + +--- + +## Cases + +### `Loading` -1. As elements of `SeamCredential.errors` — the list of active errors on a credential. -2. As elements of the `errors` list inside `SeamError.CredentialErrors`, thrown by `SeamSDK.getInstance().unlock()`. +The credential is still loading. This error is usually thrown when trying to unlock +with a credential that was not fully loaded. ```kotlin -sealed class SeamCredentialError : SeamError() { - class Loading : SeamCredentialError() - class Expired : SeamCredentialError() - data class UserInteractionRequired( - val interaction: SeamRequiredUserInteraction - ) : SeamCredentialError() - class Unknown : SeamCredentialError() -} +class Loading : SeamCredentialError() ``` ---- +### `Expired` -## Subtypes +The credential has expired. This error is usually thrown when trying to unlock +with a credential that has expired. -| Subtype | Description | -|---------|-------------| -| `Loading` | The credential has not finished loading yet. Retry the operation shortly. | -| `Expired` | The credential has expired and can no longer be used for unlocking. | -| `UserInteractionRequired(val interaction: SeamRequiredUserInteraction)` | The user must take a specific action before this credential can be used. The `interaction` property describes what action is required. | -| `Unknown` | An unclassified or unexpected credential error occurred. | +```kotlin +class Expired : SeamCredentialError() +``` ### `UserInteractionRequired` +The credential requires user interaction to be unlocked. This error is usually thrown +when trying to unlock a credential that requires user interaction, such as completing +an OTP authorization or enabling Bluetooth. + ```kotlin data class UserInteractionRequired( - val interaction: SeamRequiredUserInteraction -) : SeamCredentialError() ``` -Indicates that a user action is needed before the credential can be used. The `interaction` property is a `SeamRequiredUserInteraction` that describes the specific action required. +**Parameters** -**See also:** [`SeamRequiredUserInteraction`](/mobile-sdks/android/reference/seam-required-user-interaction) +| Parameter | Description | +| --- | --- | +| `interaction` | the required user interaction | ---- +### `Unknown` -## Example +An unknown credential error occurred. ```kotlin -lifecycleScope.launch { - SeamSDK.getInstance().credentials.collect { credentialsList -> - credentialsList.forEach { credential -> - credential.errors.forEach { error -> - when (error) { - is SeamCredentialError.Expired -> { - showExpiredBanner(credential) - } - is SeamCredentialError.Loading -> { - // Not ready yet — the SDK will emit an update when loaded - showLoadingIndicator(credential) - } - is SeamCredentialError.UserInteractionRequired -> { - handleUserInteraction(error.interaction) - } - is SeamCredentialError.Unknown -> { - showGenericError(credential) - } - } - } - } - } -} +class Unknown : SeamCredentialError() ``` - -**See also:** [`SeamError.CredentialErrors`](/mobile-sdks/android/reference/seam-error), [`SeamRequiredUserInteraction`](/mobile-sdks/android/reference/seam-required-user-interaction) diff --git a/mintlify-docs/mobile-sdks/android/reference/seam-credential.mdx b/mintlify-docs/mobile-sdks/android/reference/seam-credential.mdx index 2598c0608..224af99f0 100644 --- a/mintlify-docs/mobile-sdks/android/reference/seam-credential.mdx +++ b/mintlify-docs/mobile-sdks/android/reference/seam-credential.mdx @@ -1,14 +1,12 @@ --- title: 'SeamCredential' -description: 'An immutable snapshot of a single access credential — its name, location, expiry, supported unlock proximities, and any active errors.' +description: 'A Seam credential object.' --- -> **Interim hand-authored reference.** This page is authored directly from the Seam Android SDK public Kotlin sources. See the [reference overview](/mobile-sdks/android/reference/index) for context. +> Auto-generated from the Seam Android SDK Kotlin sources. Do not edit by hand — see `mintlify-codegen/android-reference/`. ## Overview -`SeamCredential` is a `data class` representing a single access credential as an immutable value snapshot. Read credentials from `SeamSDK.getInstance().credentials`; never construct them manually in production — the SDK manages their lifecycle. - ```kotlin data class SeamCredential( val id: String?, @@ -19,93 +17,27 @@ data class SeamCredential( val cardNumber: String?, val code: String?, val errors: List, - val isManaged: Boolean = true, + val isManaged: Boolean, val providerName: String ) ``` -**Notes** - -- Instances are immutable snapshots. To get the latest state, collect `SeamSDK.getInstance().credentials` or call `SeamSDK.getInstance().refresh()`. -- Managed credentials (`isManaged = true`) are created and controlled by the Seam API. Unmanaged credentials (`isManaged = false`) are discovered directly through a provider integration running on-device. -- `id` is nullable — always null-check before passing it to `unlock()`. +A Seam credential object. It contains information about a credential, such as its ID, name, +location, expiration date, and any errors that it encountered. --- ## Properties | Property | Type | Description | -|----------|------|-------------| -| `id` | `String?` | Unique identifier for this credential. May be `null` for some provider types. | -| `name` | `String` | Display name for this credential. | -| `location` | `String?` | Human-readable location associated with the credential (for example, building or door name). | -| `expiry` | `LocalDateTime?` | Expiration date and time, if the credential has one. | -| `cardNumber` | `String?` | Card number associated with the credential, if any. | -| `code` | `String?` | Access code associated with the credential, if any. | -| `errors` | `List` | Active errors affecting this credential. Check this list before attempting to unlock. | -| `supportedUnlockProximities` | `List` | Proximity levels this credential supports, ordered by preference. | -| `isManaged` | `Boolean` | `true` if this credential was created and managed by the Seam API; `false` if discovered by a provider integration. Defaults to `true`. | -| `providerName` | `String` | Identifies the access control provider that issued this credential. | - ---- - -### `supportedUnlockProximities` — Proximity requirements - -`supportedUnlockProximities` expresses the proximity levels acceptable for an unlock using this credential. The list is **ordered by preference**: - -- When `SeamSDK.getInstance().unlock()` is called **without** an `unlockProximity` argument, the **first** value in this list is used as the default. -- Pass any supported value explicitly via the `unlockProximity` parameter. - -**See also:** [`UnlockProximity`](/mobile-sdks/android/reference/unlock-proximity), [`SeamSDK.unlock()`](/mobile-sdks/android/reference/seam-sdk) - ---- - -### `providerName` — Known values - -The `providerName` field identifies the access control provider that issued the credential. Known values: - -| Value | Provider | -|-------|----------| -| `"hid_origo_credential_service"` | HID Origo | -| `"salto_ks"` | Salto KS | -| `"brivo"` | Brivo | -| `"assa_abloy_credential_service"` | ASSA ABLOY Credential Service | -| `"visionline_system"` | ASSA ABLOY Visionline | -| `"latch"` | Latch | -| `"assa_abloy_vostio"` | ASSA ABLOY Vostio | -| `"assa_abloy_vostio_credential_service"` | ASSA ABLOY Vostio Credential Service | -| `"salto_space"` | Salto Space | - -Additional providers may be added in future SDK releases. Treat unrecognized values gracefully. - ---- - -## Example - -```kotlin -lifecycleScope.launch { - SeamSDK.getInstance().credentials.collect { credentials -> - credentials.forEach { credential -> - // Check for errors before attempting to unlock - if (credential.errors.isEmpty()) { - val credentialId = credential.id ?: return@forEach - SeamSDK.getInstance().unlock(credentialId = credentialId) - } else { - // Handle credential errors - credential.errors.forEach { error -> - when (error) { - is SeamCredentialError.Expired -> showExpiredBanner(credential) - is SeamCredentialError.Loading -> showLoadingIndicator(credential) - is SeamCredentialError.UserInteractionRequired -> { - handleUserInteraction(error.interaction) - } - is SeamCredentialError.Unknown -> showGenericError(credential) - } - } - } - } - } -} -``` - -**See also:** [`SeamCredentialError`](/mobile-sdks/android/reference/seam-credential-error) +| --- | --- | --- | +| `id` | `String?` | The ID of the credential. | +| `supportedUnlockProximities` | `List` | The list of unlock proximities supported by the credential. Possible values in the list: UnlockProximity.TOUCH UnlockProximity.NEARBY UnlockProximity.REMOTE Use one of these values to pass to `SeamSDK.unlock` | +| `name` | `String` | A human-readable name for the credential. | +| `location` | `String?` | A human-readable location for the credential. | +| `expiry` | `LocalDateTime?` | The date and time when the credential expires. | +| `cardNumber` | `String?` | The card number associated with the credential. | +| `code` | `String?` | The code associated with the credential. | +| `errors` | `List` | A list of errors that the credential encountered. | +| `isManaged` | `Boolean` | Indicates whether this credential was created and managed by the Seam API (`true`), or was discovered directly through the provider integration (`false`). | +| `providerName` | `String` | The provider name as String. Possible values: "hid_origo_credential_service" "salto_ks" "brivo" "assa_abloy_credential_service" "visionline_system" "latch" "assa_abloy_vostio" "assa_abloy_vostio_credential_service" "salto_space" | diff --git a/mintlify-docs/mobile-sdks/android/reference/seam-error.mdx b/mintlify-docs/mobile-sdks/android/reference/seam-error.mdx index 413c40fe2..979e8c30c 100644 --- a/mintlify-docs/mobile-sdks/android/reference/seam-error.mdx +++ b/mintlify-docs/mobile-sdks/android/reference/seam-error.mdx @@ -1,115 +1,133 @@ --- title: 'SeamError' -description: 'Errors thrown by SeamSDK methods for initialization, activation, credential, and network failures.' +description: 'Base class for all Seam errors.' --- -> **Interim hand-authored reference.** This page is authored directly from the Seam Android SDK public Kotlin sources. See the [reference overview](/mobile-sdks/android/reference/index) for context. +> Auto-generated from the Seam Android SDK Kotlin sources. Do not edit by hand — see `mintlify-codegen/android-reference/`. ## Overview -`SeamError` is a `sealed class` that extends `Exception`. It is the base class for all errors thrown by `SeamSDK`. Catch `SeamError` in a `try/catch` block when calling any SDK method. - ```kotlin sealed class SeamError : Exception() ``` +Base class for all Seam errors. + +These errors are thrown by Seam classes and interfaces. They are all subclasses of +`SeamError`. + --- -## Subtypes - -| Subtype | Description | -|---------|-------------| -| `InitializationRequired` | `SeamSDK.initialize()` has not been called. Call it before using any other SDK method. | -| `ActivationRequired` | The SDK has not been activated. Call `SeamSDK.getInstance().activate()` first. | -| `IntegrationNotFound` | No provider integration was found for the specified credential. Typically thrown when the integration package (Assa Abloy, Latch, Salto, etc.) is not included in the app. | -| `AlreadyInitialized` | `SeamSDK.initialize()` was called while the SDK is already initialized. Call `deactivate()` first to reinitialize. | -| `DeactivationInProgress` | A deactivation is already in progress. Wait for it to complete before calling other methods. | -| `InvalidCredentialId` | The credential ID passed to `unlock()` is not recognized by the SDK. | -| `CredentialErrors(val errors: List)` | The credential has one or more unresolved errors. Inspect the `errors` list before attempting to unlock. | -| `InternetConnectionRequired` | An internet connection is required for this operation. | -| `InvalidClientSessionToken(override val message: String)` | The client session token passed to `initialize()` is malformed or invalid. | -| `InvalidUnlockProximity` | The `unlockProximity` passed to `unlock()` is not supported by the credential. | -| `Unknown` | An unexpected or unclassified error occurred. | +## Cases + +### `InitializationRequired` + +The SDK has not been initialized. Call `SeamSDK.initialize` first. Usually +thrown when trying to call a method that requires the SDK to be initialized, +such as `SeamSDK.unlock` or `SeamSDK.activate`. + +```kotlin +class InitializationRequired : SeamError() +``` + +### `ActivationRequired` + +The app user's phone has not been activated. Call `SeamSDK.activate` first. + +```kotlin +class ActivationRequired : SeamError() +``` + +### `IntegrationNotFound` + +No integration found for the specified credential. Usually thrown when trying to +unlock a credential that doesn't have an integration (Assa Abloy, Latch, Salto, etc) +associated with it. + +```kotlin +class IntegrationNotFound : SeamError() +``` + +### `AlreadyInitialized` + +The SDK is already initialized. Thrown when trying to call +`SeamSDK.initialize` multiple times. You can call `SeamSDK.deactivate` first. + +```kotlin +class AlreadyInitialized : SeamError() +``` + +### `DeactivationInProgress` + +A deactivation is already in progress. Throw when trying to call +`SeamSDK.deactivate` multiple times. + +```kotlin +class DeactivationInProgress : SeamError() +``` + +### `InvalidCredentialId` + +The credential id is invalid. This error is usually thrown when trying to unlock +with a credential that is invalid, or not recognized by the SDK. + +```kotlin +class InvalidCredentialId : SeamError() +``` ### `CredentialErrors` +There are multiple credential errors. Thrown when trying to unlock with a credential. +Check the `errors` list for more details. + ```kotlin class CredentialErrors(val errors: List) : SeamError() ``` -Thrown by `unlock()` when the target credential has one or more active errors that prevent unlocking. The `errors` list contains `SeamCredentialError` instances in priority order. +**Parameters** + +| Parameter | Description | +| --- | --- | +| `errors` | the list of errors | -**See also:** [`SeamCredentialError`](/mobile-sdks/android/reference/seam-credential-error) +### `InternetConnectionRequired` + +The device does not have an internet connection. Usually thrown when trying to +call a method that requires an internet connection, such as `SeamSDK.unlock`, +`SeamSDK.initialize`, `SeamSDK.refresh`, `SeamSDK.activate` etc. + +```kotlin +class InternetConnectionRequired : SeamError() +``` ### `InvalidClientSessionToken` +The client session token is invalid. Trhown when trying to call +`SeamSDK.initialize` with an invalid client session token. + ```kotlin class InvalidClientSessionToken( - override val message: String = "Invalid client session token" -) : SeamError() ``` -Thrown by `initialize()` when the provided token is malformed. The `message` property contains a human-readable description of the problem. +**Parameters** ---- +| Parameter | Description | +| --- | --- | +| `message` | the error message | -## Example — Handling `unlock()` errors +### `InvalidUnlockProximity` + +The unlock proximity is invalid. Thrown when trying to unlock with an invalid unlock +proximity. ```kotlin -try { - SeamSDK.getInstance().unlock( - credentialId = credential.id!!, - unlockProximity = UnlockProximity.TOUCH - ) -} catch (seamError: SeamError) { - when (seamError) { - is SeamError.InitializationRequired -> { - // SDK not initialized — call SeamSDK.initialize() first - } - is SeamError.ActivationRequired -> { - // SDK not activated — call seamSDK.activate() first - } - is SeamError.IntegrationNotFound -> { - // Provider SDK (e.g. Assa Abloy, Latch, Salto) not found - // Check that the correct integration module is included - } - is SeamError.InvalidCredentialId -> { - // The credential ID is not recognized - } - is SeamError.CredentialErrors -> { - // Credential has unresolved errors — inspect seamError.errors - seamError.errors.forEach { credentialError -> - when (credentialError) { - is SeamCredentialError.Expired -> { - // credential is expired - } - is SeamCredentialError.Loading -> { - // credential not fully loaded yet — retry shortly - } - is SeamCredentialError.UserInteractionRequired -> { - handleUserInteraction(credentialError.interaction) - } - is SeamCredentialError.Unknown -> { - // unknown credential error - } - } - } - } - is SeamError.InvalidUnlockProximity -> { - // unlockProximity not supported by this credential - // Check credential.supportedUnlockProximities - } - is SeamError.InternetConnectionRequired -> { - // No internet — offline unlock may not be possible for this credential - } - is SeamError.Unknown -> { - // Unexpected error - } - else -> { - // Handle any remaining cases - } - } -} +class InvalidUnlockProximity : SeamError() ``` -**See also:** [`SeamCredentialError`](/mobile-sdks/android/reference/seam-credential-error), [`SeamSDK`](/mobile-sdks/android/reference/seam-sdk) +### `Unknown` + +An unknown error occurred. + +```kotlin +class Unknown : SeamError() +``` diff --git a/mintlify-docs/mobile-sdks/android/reference/seam-required-user-interaction.mdx b/mintlify-docs/mobile-sdks/android/reference/seam-required-user-interaction.mdx index 1af3e1765..1ee73eb49 100644 --- a/mintlify-docs/mobile-sdks/android/reference/seam-required-user-interaction.mdx +++ b/mintlify-docs/mobile-sdks/android/reference/seam-required-user-interaction.mdx @@ -1,81 +1,68 @@ --- title: 'SeamRequiredUserInteraction' -description: 'Actions the user must take to resolve a SeamCredentialError.UserInteractionRequired error — OTP authorization, enabling internet or Bluetooth, or granting permissions.' +description: 'A user interaction that is required to unlock a credential.' --- -> **Interim hand-authored reference.** This page is authored directly from the Seam Android SDK public Kotlin sources. See the [reference overview](/mobile-sdks/android/reference/index) for context. +> Auto-generated from the Seam Android SDK Kotlin sources. Do not edit by hand — see `mintlify-codegen/android-reference/`. ## Overview -`SeamRequiredUserInteraction` is a `sealed class` that describes the specific action a user must take to resolve a `SeamCredentialError.UserInteractionRequired` error. Inspect the subtype to determine what to prompt the user to do. - ```kotlin -sealed class SeamRequiredUserInteraction { - data class CompleteOtpAuthorization(val otpUrl: URL) : SeamRequiredUserInteraction() - class EnableInternet : SeamRequiredUserInteraction() - class EnableBluetooth : SeamRequiredUserInteraction() - class GrantPermissions(val permissions: List) : SeamRequiredUserInteraction() -} +sealed class SeamRequiredUserInteraction ``` ---- +A user interaction that is required to unlock a credential. This can be one of the following: +- `CompleteOtpAuthorization`: complete an OTP authorization to unlock the credential. +- `EnableInternet`: enable internet to unlock the credential. +- `EnableBluetooth`: enable Bluetooth to unlock the credential. +- `GrantPermissions`: grant the required permissions to unlock the credential. -## Subtypes +--- -| Subtype | Description | -|---------|-------------| -| `CompleteOtpAuthorization(val otpUrl: URL)` | The user must complete OTP authorization at the provided URL to unlock with this credential. Open `otpUrl` in a browser. | -| `EnableInternet` | The user must enable internet connectivity on their device. | -| `EnableBluetooth` | The user must enable Bluetooth on their device. | -| `GrantPermissions(val permissions: List)` | The user must grant one or more permissions to the app. The `permissions` list contains the Android permission strings that need to be granted. | +## Cases ### `CompleteOtpAuthorization` +Complete an OTP authorization to unlock with the credential. The user needs to go to +`otpUrl` to complete the authorization. + ```kotlin data class CompleteOtpAuthorization(val otpUrl: URL) : SeamRequiredUserInteraction() ``` -The user must visit `otpUrl` in a browser to complete a one-time-password authorization step. This is required by certain access control providers before their credentials can be used. +**Parameters** -### `GrantPermissions` +| Parameter | Description | +| --- | --- | +| `otpUrl` | the URL to complete the OTP authorization | + +### `EnableInternet` + +Enable internet to unlock with the credential. The user needs to enable internet on their device. ```kotlin -class GrantPermissions(val permissions: List) : SeamRequiredUserInteraction() +class EnableInternet : SeamRequiredUserInteraction() ``` -The `permissions` list contains Android permission strings (for example, `"android.permission.BLUETOOTH_SCAN"`) that the app must request from the user via the standard Android permissions API. +### `EnableBluetooth` ---- +Enable Bluetooth to unlock the credential. The user needs to enable Bluetooth on their device. + +```kotlin +class EnableBluetooth : SeamRequiredUserInteraction() +``` -## Example +### `GrantPermissions` + +Grant the required permissions to unlock with the credential. The user needs to grant the +required permissions on their device. ```kotlin -fun handleUserInteraction(interaction: SeamRequiredUserInteraction) { - when (interaction) { - is SeamRequiredUserInteraction.CompleteOtpAuthorization -> { - // Open the OTP URL in a browser or WebView - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(interaction.otpUrl.toString())) - startActivity(intent) - } - is SeamRequiredUserInteraction.EnableInternet -> { - // Prompt the user to enable Wi-Fi or mobile data - showEnableInternetDialog() - } - is SeamRequiredUserInteraction.EnableBluetooth -> { - // Prompt the user to enable Bluetooth - val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) - startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) - } - is SeamRequiredUserInteraction.GrantPermissions -> { - // Request the required permissions from the user - ActivityCompat.requestPermissions( - this, - interaction.permissions.toTypedArray(), - REQUEST_PERMISSIONS - ) - } - } -} +class GrantPermissions(val permissions: List) : SeamRequiredUserInteraction() ``` -**See also:** [`SeamCredentialError.UserInteractionRequired`](/mobile-sdks/android/reference/seam-credential-error) +**Parameters** + +| Parameter | Description | +| --- | --- | +| `permissions` | the permissions that need to be granted | diff --git a/mintlify-docs/mobile-sdks/android/reference/seam-sdk.mdx b/mintlify-docs/mobile-sdks/android/reference/seam-sdk.mdx index 0393e8715..4cd5b862b 100644 --- a/mintlify-docs/mobile-sdks/android/reference/seam-sdk.mdx +++ b/mintlify-docs/mobile-sdks/android/reference/seam-sdk.mdx @@ -1,59 +1,67 @@ --- title: 'SeamSDK' -description: 'The main entry point for the Seam Android SDK. Manages SDK lifecycle, credential synchronization, unlock operations, and StateFlow-based status updates.' +description: 'This is the main entry point for the Seam Android SDK.' --- -> **Interim hand-authored reference.** This page is authored directly from the Seam Android SDK public Kotlin sources. See the [reference overview](/mobile-sdks/android/reference/index) for context. +> Auto-generated from the Seam Android SDK Kotlin sources. Do not edit by hand — see `mintlify-codegen/android-reference/`. ## Overview -`SeamSDK` is the single entry point for all Seam Android SDK operations. It is a `class` with a private constructor — obtain the instance via `SeamSDK.getInstance()` after calling `SeamSDK.initialize()`. - ```kotlin -class SeamSDK private constructor() +class SeamSDK() ``` -**Thread Safety** +This is the main entry point for the Seam Android SDK. -- All `StateFlow` properties (`credentials`, `isActivated`, `unlockStatus`) are thread-safe. -- All suspend functions can be called from any coroutine context. -- `initialize` is thread-safe and idempotent. +Use the `getInstance` function to get the instance of this class. +You can only get the instance of the class after you have initialized the SDK by calling +`initialize`. -**Offline Behavior** +The `initialize` function will initialize the SDK. -- The SDK caches credentials and can work offline for unlock operations. -- `activate()` requires an internet connection on first activation. -- `refresh()` always requires an internet connection. -- `unlock()` can work offline if credentials are already cached. +## Thread Safety +- All StateFlow properties (`credentials`, `isActivated`, `unlockStatus`) are thread-safe +- All suspend functions can be called from any coroutine context +- `initialize` is thread-safe and idempotent ---- +## StateFlow Lifecycle +- `credentials`: Emits whenever credential list changes, survives configuration changes +- `unlockStatus`: Emits unlock events, latest event is always available via .value +- `isActivated`: Reflects current SDK activation state -## Companion Object Methods +Collecting these flows is lifecycle-safe when used with Android lifecycle-aware scopes -### `initialize(context, clientSessionToken)` +## Offline Behavior +- SDK caches credentials and can work offline for unlock operations +- `activate` requires internet connection initially +- `refresh` requires internet connection +- `unlock` can work offline if credentials are already cached -Initializes the Seam Android SDK. Call this once at app launch, typically in `Application.onCreate()`. +## Error Recovery +- Most operations can be safely retried after fixing underlying issues +- Credential errors (in SeamCredential.errors) should be resolved before unlock +- Network errors during activate/refresh are transient and can be retried -```kotlin -suspend fun initialize(context: Context, clientSessionToken: String) -``` +## Background Processing +- Unlock operations continue in background +- StateFlow emissions work across app lifecycle -**Parameters** +## Lifecycle +- Call `initialize` once per app session, typically in Application.onCreate() +- `activate` when user wants to use credential features +- No explicit cleanup needed - Android handles resource cleanup +- `deactivate` stops background operations but preserves credentials -| Parameter | Type | Description | -|-----------|------|-------------| -| `context` | `Context` | The Android application context. | -| `clientSessionToken` | `String` | A valid client session token for the app user. | +--- -**Throws** +## Companion object methods -| Error | Description | -|-------|-------------| -| `SeamError.AlreadyInitialized` | The SDK is already initialized. Call `deactivate()` first to reinitialize. | -| `SeamError.InternetConnectionRequired` | No internet connection is available. | -| `SeamError.InvalidClientSessionToken` | The token format is invalid. | -| `SeamError.DeactivationInProgress` | A deactivation is already in progress. | +### `initialize` +Initializes the Seam Android SDK. This should be called once at the +start of your application. + +Example: ```kotlin try { SeamSDK.initialize(context, "seam_cst_...") @@ -68,7 +76,7 @@ try { is SeamError.InternetConnectionRequired -> { // handle error when internet connection is required } - is SeamError.InvalidClientSessionToken -> { + is SeamError.InvalidClientSessionToken -> { // handle error when client session token is invalid } else -> { @@ -78,199 +86,318 @@ try { } ``` ---- - -### `getInstance()` - -Returns the singleton instance of `SeamSDK`. Thread-safe; can be called from any thread. - ```kotlin -fun getInstance(): SeamSDK +suspend fun initialize(context: Context, clientSessionToken: String) ``` -**Returns:** The initialized `SeamSDK` instance. +**Parameters** + +| Parameter | Description | +| --- | --- | +| `context` | the Android context. | +| `clientSessionToken` | the client session token for the app user. | **Throws** -| Error | Description | -|-------|-------------| -| `SeamError.InitializationRequired` | `initialize()` has not been called yet. | +- `SeamError.AlreadyInitialized` — if the SDK is already initialized. +- `SeamError.InternetConnectionRequired` — if no internet connection is available. +- `SeamError.InvalidClientSessionToken` — if the token format is invalid. +- `SeamError.DeactivationInProgress` — if a deactivation is in progress. + +### `getInstance` + +Returns the instance of `SeamSDK`. This can be used to get a handle to the +SeamSDK after it has been initialized. + +This method is thread-safe and can be called from any thread. ```kotlin -val seamSDK = SeamSDK.getInstance() +fun getInstance() : SeamSDK ``` +**Returns** — The initialized SeamSDK instance. + +**Throws** + +- `SeamError.InitializationRequired` — if `initialize` has not been called yet. + --- -## StateFlow Properties +## Properties -### `credentials` +### `isActivated` -The current list of `SeamCredential` objects for the app user. Emits an updated list whenever credentials change. +Returns whether the app user's phone has been activated. + +This StateFlow emits `true` after successful `activate` call and `false` after `deactivate`. +The flow is thread-safe and can be collected from any coroutine context. +Latest value is always available via `.value` property. ```kotlin -val credentials: StateFlow> +val isActivated ``` -The list may be empty while loading or if no credentials are available. Check each credential's `errors` property to determine if it is ready for use. Latest value is always available via `.value`. +### `credentials` + +Returns the list of `SeamCredential` for the current app user. + +This StateFlow emits an immutable list whenever credentials change. +The list may be empty if no credentials are available or while loading. +Check each credential's `errors` property to determine if it's ready for use. + +The flow is thread-safe and can be collected from any coroutine context. +Latest value is always available via `.value` property. ```kotlin -// Observe credential changes -lifecycleScope.launch { - seamSDK.credentials.collect { credentials -> - if (credentials.isEmpty()) { - // handle no credentials - } else { - // display credentials - } +// Observe credentials +seamSDK.credentials.collect { credentials -> + // credentials is a list of [SeamCredential] + if (credentials.isEmpty()) { + // handle no credentials + } else { + // handle credentials (e.g. display them) } } -// Handle credential errors -lifecycleScope.launch { - SeamSDK.getInstance().credentials.collect { credentialsList -> - val errors = credentialsList.flatMap { it.errors } - errors.forEach { error -> - when (error) { - is SeamCredentialError.Expired -> { /* handle expiration */ } - is SeamCredentialError.Loading -> { /* handle not loaded yet */ } - is SeamCredentialError.Unknown -> { /* handle unknown error */ } - is SeamCredentialError.UserInteractionRequired -> { - handleUserInteractionRequired(error.interaction) - } +// handle credential errors +SeamSDK.getInstance().credentials.collect { credentialsList -> + val errors = credentialsList.flatMap { it.errors } + errors.forEach { error -> + when (error) { + is SeamCredentialError.Expired -> { /* handle credential expiration error */ } + is SeamCredentialError.Loading -> { /* handle not loaded yet */ } + is SeamCredentialError.Unknown -> { /* handle unknown error */ } + is SeamCredentialError.UserInteractionRequired -> { + handleUserInteractionRequired(error.interaction) } } } } -``` -**See also:** [`SeamCredential`](/mobile-sdks/android/reference/seam-credential) +fun handleUserInteractionRequired(interaction: SeamRequiredUserInteraction) { + when (interaction) { + is SeamRequiredUserInteraction.CompleteOtpAuthorization -> { /* handle OTP authorization */ } + is SeamRequiredUserInteraction.EnableBluetooth -> { /* handle Bluetooth error */ } + is SeamRequiredUserInteraction.EnableInternet -> { /* handle Internet connection error*/ } + is SeamRequiredUserInteraction.GrantPermissions -> { /* handle permissions error*/ } + } +} +``` ---- +```kotlin +val credentials +``` -### `isActivated` +### `unlockStatus` -Whether the SDK is currently activated. Emits `true` after a successful `activate()` call and `false` after `deactivate()`. +Returns the current status of the unlock feature. -```kotlin -val isActivated: StateFlow -``` +This StateFlow emits `SeamUnlockEvent` instances as unlock operations progress. +Events include scanning, connection, access granted, errors, and timeout. +The flow retains the latest event, accessible via `.value` property. -Latest value is always available via `.value`. +Subscribe to this flow before calling `unlock` to ensure no events are missed. +The flow is thread-safe and can be collected from any coroutine context. ```kotlin -lifecycleScope.launch { - seamSDK.isActivated.collect { activated -> - updateActivationUI(activated) - } -} +val unlockStatus ``` --- -### `unlockStatus` +## Methods + +### `setUnlockEventListener` -The current status of the unlock operation. Emits `SeamUnlockEvent` instances as unlock operations progress. +Sets the listener for the unlock events. ```kotlin -val unlockStatus: StateFlow +fun setUnlockEventListener(listener: (SeamUnlockEvent) -> Unit) ``` -The flow retains the latest event, accessible via `.value`. Subscribe to this flow **before** calling `unlock()` to ensure no events are missed. +**Parameters** + +| Parameter | Description | +| --- | --- | +| `listener` | the listener to listen for unlock events. | + +### `setNotification` + +Sets the notification for the unlock feature. Required for Assa Abloy foreground scanning. ```kotlin -lifecycleScope.launch { - seamSDK.unlockStatus.collect { event -> - when (event) { - is SeamUnlockEvent.ScanningStarted -> showScanningUI() - is SeamUnlockEvent.AccessGranted -> showSuccessUI() - is SeamUnlockEvent.Timeout -> showTimeoutError() - is SeamUnlockEvent.ReaderError -> showReaderError(event.message) - } - } -} +fun setNotification(notification: Notification) ``` -**See also:** [`SeamUnlockEvent`](/mobile-sdks/android/reference/seam-unlock-event) +**Parameters** + +| Parameter | Description | +| --- | --- | +| `notification` | the notification to use for the unlock feature. | ---- +### `setCredentialsListener` + +Sets the listener for the credentials. + +```kotlin +fun setCredentialsListener(listener: (List) -> Unit) +``` -## Suspend Methods +**Parameters** -### `activate()` +| Parameter | Description | +| --- | --- | +| `listener` | the listener to listen for credentials. | -Activates the SDK by syncing with the server and setting up background services. Requires an active internet connection on first activation. Safe to call multiple times — subsequent calls are no-ops if already activated. +### `listCredentials` -After successful activation, `isActivated` emits `true` and credentials begin syncing automatically. +Lists the credentials for the current app user. ```kotlin -suspend fun activate() +fun listCredentials() : List ``` -**Throws** +**Returns** — a SeamResult of a list of AcsPhoneCredential. If the SDK has not been initialized yet, the result will be a SeamFailure with `SeamError.InitializationRequired`. + +### `unlock` + +Unlocks the app user's phone. -| Error | Description | -|-------|-------------| -| `SeamError.InitializationRequired` | `initialize()` has not been called. | -| `SeamError.InternetConnectionRequired` | No internet connection is available. | -| `SeamError.InvalidClientSessionToken` | The client session token is invalid. | +Initiates an unlock operation for the specified credential. Progress events are emitted +to `unlockStatus` StateFlow. The operation continues even if the app goes to background. + +Works offline if credentials are already cached. Requires the device to have the +necessary hardware (Bluetooth, NFC, etc.) and permissions granted. ```kotlin val seamSDK = SeamSDK.getInstance() +// Start collecting unlock events before unlock +coroutineScope.launch { + seamSDK.unlockStatus.collect { event -> + when (event) { + is SeamUnlockEvent.ScanningStarted -> { /* handle scanning started */ } + is SeamUnlockEvent.Connecting -> { /* handle connecting */ } + is SeamUnlockEvent.AccessGranted -> { /* handle access granted */ } + is SeamUnlockEvent.Timeout -> { /* handle timeout */ } + is SeamUnlockEvent.ReaderError -> { /* handle reader error */ } + else -> { /* handle other events */ } + } + } +} + +// Perform unlock try { - seamSDK.activate() + val credentialId = credential.id + // Timeout is optional + seamSDK.unlock( + credentialId = credentialId, + unlockProximity = UnlockProximity.TOUCH, + timeout = 30.seconds + ) } catch (seamError: SeamError) { when (seamError) { + is SeamError.ActivationRequired -> { + // handle error when SDK is not activated + } + is SeamError.CredentialErrors -> { + val credentialErrors = seamError.errors + handleCredentialErrors(credentialErrors) + // handle error when there are credential errors + } is SeamError.InitializationRequired -> { // handle error when SDK is not initialized } - is SeamError.InternetConnectionRequired -> { - // handle error when internet connection is required + is SeamError.IntegrationNotFound -> { + // handle error when integration is not found, Such as Assa Abloy, Latch and Salto } - is SeamError.InvalidClientSessionToken -> { - // handle error when client session token is invalid + is SeamError.InvalidCredentialId -> { + // handle error when credential ID is invalid } else -> { // handle other errors } } } -``` ---- +// Handle credential errors on unlock +fun handleCredentialErrors(credentialErrors: List) { + credentialErrors.forEach { credentialError -> + when (credentialError) { + is SeamCredentialError.Invalid -> { + // handle error when credential is invalid + } + + is SeamCredentialError.Expired -> { + // handle error when credential is expired + } -### `deactivate(deintegrate)` + is SeamCredentialError.Loading -> { + // handle error when credential is not loaded yet + } -Deactivates the SDK. Stops background operations and releases resources. Credentials remain cached and available for the next activation. Safe to call multiple times. + is SeamCredentialError.UserInteractionRequired -> { + // handle user interaction required credential error + } + is SeamCredentialError.InvalidUnlockProximity -> { + // handle invalid unlock proximity credential error + } + is SeamCredentialError.Unknown -> { + // handle unknown credential error + } + } + } +} +``` ```kotlin -suspend fun deactivate(deintegrate: Boolean = false) +fun unlock(credentialId: CredentialId, unlockProximity: UnlockProximity? = null, timeout: Duration? = null) : Job ``` **Parameters** -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `deintegrate` | `Boolean` | `false` | If `true`, removes the device association and clears all state. | +| Parameter | Description | +| --- | --- | +| `credentialId` | the credential ID to unlock. CredentialId is a type alias for String. | +| `timeout` | the timeout for the unlock operation. If null, uses provider default. | +| `unlockProximity` | the unlock proximity `UnlockProximity`. If null, uses provider default, which is the first in the list of `SeamCredential.supportedUnlockProximities`. The `SeamCredential` must have at least one supported unlock proximity. | + +**Returns** — a Job that can be used to cancel the unlock operation. **Throws** -| Error | Description | -|-------|-------------| -| `SeamError.InitializationRequired` | `initialize()` has not been called. | -| `SeamError.DeactivationInProgress` | A deactivation is already in progress. | +- `SeamError.InitializationRequired` — if the SDK has not been initialized yet. +- `SeamError.IntegrationNotFound` — if the integration is not found. +- `SeamError.InvalidCredentialId` — if the credential ID is invalid. +- `SeamError.ActivationRequired` — if the app user's phone has not been activated yet. +- `SeamError.CredentialErrors` — if the credential has unresolved errors. +- `SeamError.InvalidUnlockProximity` — if the unlock proximity is invalid. + +### `suspend activate` + +Activates the app user's phone. + +Prepares the SDK for use by syncing with the server and setting up background services. +This operation requires an active internet connection on first activation. +Safe to call multiple times - subsequent calls are no-ops if already activated. + +After successful activation, `isActivated` will emit `true` and credentials will +start syncing automatically. ```kotlin val seamSDK = SeamSDK.getInstance() try { - seamSDK.deactivate(deintegrate = true) + // activate is a suspend function + seamSDK.activate() } catch (seamError: SeamError) { when (seamError) { is SeamError.InitializationRequired -> { - // handle error when SDK is not initialized + // handle error when SDK is already initialized } - is SeamError.DeactivationInProgress -> { - // handle error when deactivation is already in progress + is SeamError.InternetConnectionRequired -> { + // handle error when internet connection is required + } + is SeamError.InvalidClientSessionToken -> { + // handle error when client session token is invalid } else -> { // handle other errors @@ -279,36 +406,37 @@ try { } ``` ---- +```kotlin +suspend fun activate() +``` -### `refresh()` +**Throws** -Manually syncs credentials with the server and updates the `credentials` StateFlow. Requires an active internet connection. In most cases, automatic background sync makes manual refresh unnecessary. +- `SeamError.InitializationRequired` — if the SDK has not been initialized yet. +- `SeamError.InternetConnectionRequired` — if no internet connection is available. +- `SeamError.InvalidClientSessionToken` — if the client session token is invalid. -```kotlin -suspend fun refresh(): List -``` +### `suspend deactivate` -**Returns:** The updated list of `SeamCredential` objects after the refresh completes. +Deactivates the app user's phone. -**Throws** +Stops background operations and cleans up resources. Credentials remain cached +and available for the next activation. After deactivation, `isActivated` will emit `false`. -| Error | Description | -|-------|-------------| -| `SeamError.InitializationRequired` | `initialize()` has not been called. | -| `SeamError.DeactivationInProgress` | A deactivation is in progress. | +This method is safe to call multiple times and will not throw if already deactivated. ```kotlin val seamSDK = SeamSDK.getInstance() try { - val updatedCredentials = seamSDK.refresh() + // deactivate is a suspend function + seamSDK.deactivate(deintegrate = true) } catch (seamError: SeamError) { when (seamError) { is SeamError.InitializationRequired -> { // handle error when SDK is not initialized } is SeamError.DeactivationInProgress -> { - // handle error when app is being deactivated + // handle error when internet connection is required } else -> { // handle other errors @@ -317,169 +445,58 @@ try { } ``` ---- - -## Non-Suspend Methods - -### `unlock(credentialId, unlockProximity?, timeout?)` - -Initiates an unlock operation for the specified credential. Progress events are emitted to the `unlockStatus` StateFlow. The operation continues even if the app moves to the background. Works offline if credentials are already cached. - ```kotlin -fun unlock( - credentialId: CredentialId, - unlockProximity: UnlockProximity? = null, - timeout: Duration? = null -): Job +suspend fun deactivate(deintegrate: Boolean = false) ``` -`CredentialId` is a type alias for `String`. - **Parameters** -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `credentialId` | `CredentialId` | — | The credential ID to unlock. | -| `unlockProximity` | `UnlockProximity?` | `null` | The required proximity for this attempt. If `null`, uses the provider default (the first value in `SeamCredential.supportedUnlockProximities`). | -| `timeout` | `Duration?` | `null` | Maximum time to wait for the unlock operation. If `null`, uses the provider default. | - -**Returns:** A `Job` that can be used to cancel the unlock operation. +| Parameter | Description | +| --- | --- | +| `deintegrate` | If true, removes device association and clears all state. | **Throws** -| Error | Description | -|-------|-------------| -| `SeamError.InitializationRequired` | `initialize()` has not been called. | -| `SeamError.ActivationRequired` | The SDK has not been activated. | -| `SeamError.IntegrationNotFound` | No integration found for the credential's provider. | -| `SeamError.InvalidCredentialId` | The credential ID is not recognized by the SDK. | -| `SeamError.CredentialErrors` | The credential has one or more unresolved errors. | -| `SeamError.InvalidUnlockProximity` | The specified unlock proximity is not supported by this credential. | +- `SeamError.InitializationRequired` — if the SDK has not been initialized yet. +- `SeamError.DeactivationInProgress` — if a deactivation is already in progress. -```kotlin -val seamSDK = SeamSDK.getInstance() +### `suspend refresh` -// Start collecting unlock events before calling unlock -coroutineScope.launch { - seamSDK.unlockStatus.collect { event -> - when (event) { - is SeamUnlockEvent.ScanningStarted -> { /* handle scanning started */ } - is SeamUnlockEvent.AccessGranted -> { /* handle access granted */ } - is SeamUnlockEvent.Timeout -> { /* handle timeout */ } - is SeamUnlockEvent.ReaderError -> { /* handle reader error */ } - } - } -} +Refreshes the credentials for the current app user. -// Perform unlock +Manually triggers a sync with the server to fetch latest credentials. +This operation requires an active internet connection. You should call +`initialize` and `activate` first. + +The `credentials` StateFlow will be updated before this method returns. +In most cases, automatic background sync makes manual refresh unnecessary. + +```kotlin +val seamSDK = SeamSDK.getInstance() try { - seamSDK.unlock( - credentialId = credential.id!!, - unlockProximity = UnlockProximity.TOUCH, - timeout = 30.seconds - ) + seamSDK.refresh() } catch (seamError: SeamError) { when (seamError) { - is SeamError.ActivationRequired -> { /* SDK not activated */ } - is SeamError.CredentialErrors -> { - handleCredentialErrors(seamError.errors) + is SeamError.InitializationRequired -> { + // handle error when SDK is not initialized + } + is SeamError.DeactivationInProgress -> { + // handle error when app is being deactivated + } + else -> { + // handle other errors } - is SeamError.InitializationRequired -> { /* SDK not initialized */ } - is SeamError.IntegrationNotFound -> { /* provider not found */ } - is SeamError.InvalidCredentialId -> { /* bad credential ID */ } - else -> { /* handle other errors */ } - } -} -``` - -**See also:** [`UnlockProximity`](/mobile-sdks/android/reference/unlock-proximity), [`SeamUnlockEvent`](/mobile-sdks/android/reference/seam-unlock-event), [`SeamError`](/mobile-sdks/android/reference/seam-error) - ---- - -### `listCredentials()` - -Returns the current credential list synchronously. If the SDK has not been initialized, returns an empty list. - -```kotlin -fun listCredentials(): List -``` - -**Returns:** The current list of `SeamCredential` objects. - -```kotlin -val credentials = seamSDK.listCredentials() -``` - ---- - -### `setUnlockEventListener(listener)` - -Registers a callback to receive unlock events as an alternative to collecting the `unlockStatus` StateFlow. - -```kotlin -fun setUnlockEventListener(listener: (SeamUnlockEvent) -> Unit) -``` - -**Parameters** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `listener` | `(SeamUnlockEvent) -> Unit` | A lambda invoked for each unlock event. | - -```kotlin -seamSDK.setUnlockEventListener { event -> - when (event) { - is SeamUnlockEvent.ScanningStarted -> showScanningUI() - is SeamUnlockEvent.AccessGranted -> showSuccessUI() - is SeamUnlockEvent.Timeout -> showTimeoutError() - is SeamUnlockEvent.ReaderError -> showReaderError(event.message) } } ``` ---- - -### `setCredentialsListener(listener)` - -Registers a callback to receive credential list updates as an alternative to collecting the `credentials` StateFlow. - -```kotlin -fun setCredentialsListener(listener: (List) -> Unit) -``` - -**Parameters** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `listener` | `(List) -> Unit` | A lambda invoked whenever the credential list changes. | - -```kotlin -seamSDK.setCredentialsListener { credentials -> - updateCredentialList(credentials) -} -``` - ---- - -### `setNotification(notification)` - -Sets the foreground service notification used during unlock scanning. Required for Assa Abloy foreground scanning. - ```kotlin -fun setNotification(notification: Notification) +suspend fun refresh() : List ``` -**Parameters** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `notification` | `Notification` | The notification to display while the foreground service is running. | +**Returns** — The updated list of credentials after refresh completes. -```kotlin -val notification = NotificationCompat.Builder(context, CHANNEL_ID) - .setContentTitle("Scanning for doors…") - .setSmallIcon(R.drawable.ic_lock) - .build() +**Throws** -seamSDK.setNotification(notification) -``` +- `SeamError.InitializationRequired` — if the SDK has not been initialized yet. +- `SeamError.DeactivationInProgress` — if a deactivation is in progress. diff --git a/mintlify-docs/mobile-sdks/android/reference/seam-unlock-event.mdx b/mintlify-docs/mobile-sdks/android/reference/seam-unlock-event.mdx index a90e62372..b4a51d868 100644 --- a/mintlify-docs/mobile-sdks/android/reference/seam-unlock-event.mdx +++ b/mintlify-docs/mobile-sdks/android/reference/seam-unlock-event.mdx @@ -1,85 +1,50 @@ --- title: 'SeamUnlockEvent' -description: 'Events emitted to the unlockStatus StateFlow as an unlock operation progresses from scanning through access granted, timeout, or reader error.' +description: 'Events related to the co.seam.core.api.SeamSDK.unlock operation.' --- -> **Interim hand-authored reference.** This page is authored directly from the Seam Android SDK public Kotlin sources. See the [reference overview](/mobile-sdks/android/reference/index) for context. +> Auto-generated from the Seam Android SDK Kotlin sources. Do not edit by hand — see `mintlify-codegen/android-reference/`. ## Overview -`SeamUnlockEvent` is a `sealed class` representing the stages of a door unlock operation. As `SeamSDK.getInstance().unlock()` progresses, the SDK emits these events to the `unlockStatus` StateFlow. - ```kotlin -sealed class SeamUnlockEvent { - class ScanningStarted : SeamUnlockEvent() - class AccessGranted : SeamUnlockEvent() - class Timeout : SeamUnlockEvent() - class ReaderError(val message: String) : SeamUnlockEvent() -} +sealed class SeamUnlockEvent ``` -Subscribe to `unlockStatus` **before** calling `unlock()` to ensure no events are missed. +Events related to the `co.seam.core.api.SeamSDK.unlock` operation. --- -## Subtypes +## Cases -| Subtype | Description | -|---------|-------------| -| `ScanningStarted` | The unlock operation has started scanning for a reader. | -| `AccessGranted` | The reader granted access; entry is allowed. | -| `Timeout` | The unlock operation timed out before completing. | -| `ReaderError(val message: String)` | An error occurred while communicating with the reader. The `message` property contains diagnostic details. | +### `ScanningStarted` -### `ReaderError` +The unlock operation has started scanning. ```kotlin -class ReaderError(val message: String) : SeamUnlockEvent() +class ScanningStarted : SeamUnlockEvent() ``` -Emitted when the SDK encounters an error communicating with the reader hardware. The `message` property contains diagnostic details intended for developers — do not display it directly to end users. +### `AccessGranted` ---- +The unlock operation has been granted access. + +```kotlin +class AccessGranted : SeamUnlockEvent() +``` -## Example +### `Timeout` + +The unlock operation has timed out. ```kotlin -val seamSDK = SeamSDK.getInstance() - -// Subscribe before calling unlock -lifecycleScope.launch { - seamSDK.unlockStatus.collect { event -> - when (event) { - is SeamUnlockEvent.ScanningStarted -> { - showScanningSpinner() - } - is SeamUnlockEvent.AccessGranted -> { - hideScanningSpinner() - showSuccessBanner() - } - is SeamUnlockEvent.Timeout -> { - hideScanningSpinner() - showTimeoutError() - } - is SeamUnlockEvent.ReaderError -> { - hideScanningSpinner() - // event.message contains diagnostic details - showReaderError() - } - } - } -} - -// Then perform the unlock -try { - seamSDK.unlock( - credentialId = credential.id!!, - unlockProximity = UnlockProximity.TOUCH - ) -} catch (seamError: SeamError) { - // Handle synchronous errors thrown before scanning begins - handleUnlockError(seamError) -} +class Timeout : SeamUnlockEvent() ``` -**See also:** [`SeamSDK.unlock()`](/mobile-sdks/android/reference/seam-sdk), [`UnlockProximity`](/mobile-sdks/android/reference/unlock-proximity) +### `ReaderError` + +An error has occurred while communicating with the reader. + +```kotlin +class ReaderError(val message: String) : SeamUnlockEvent() +``` diff --git a/mintlify-docs/mobile-sdks/android/reference/unlock-proximity.mdx b/mintlify-docs/mobile-sdks/android/reference/unlock-proximity.mdx index 72e1e9c1e..2c0ec7942 100644 --- a/mintlify-docs/mobile-sdks/android/reference/unlock-proximity.mdx +++ b/mintlify-docs/mobile-sdks/android/reference/unlock-proximity.mdx @@ -1,59 +1,30 @@ --- title: 'UnlockProximity' -description: 'The required physical proximity for an unlock attempt: TOUCH, NEARBY, or REMOTE.' +description: 'Represents the proximity at which a credential used to unlock a door.' --- -> **Interim hand-authored reference.** This page is authored directly from the Seam Android SDK public Kotlin sources. See the [reference overview](/mobile-sdks/android/reference/index) for context. +> Auto-generated from the Seam Android SDK Kotlin sources. Do not edit by hand — see `mintlify-codegen/android-reference/`. ## Overview -`UnlockProximity` is an `enum class` representing the proximity at which a credential is used to unlock a reader. Pass one of these values to `SeamSDK.getInstance().unlock()` to specify the required proximity for an attempt. - ```kotlin -enum class UnlockProximity { - TOUCH, - NEARBY, - REMOTE -} +enum class UnlockProximity ``` -If no `unlockProximity` argument is provided to `unlock()`, the SDK uses the first value in `SeamCredential.supportedUnlockProximities` as the default. +Represents the proximity at which a credential used to unlock a door. --- -## Cases +## Values -| Case | Description | -|------|-------------| -| `TOUCH` | The user must hold the back of the phone against the reader to unlock. | -| `NEARBY` | The user must be physically close to the reader but does not need to touch it. | -| `REMOTE` | The reader is unlocked from a remote location, for example via Wi-Fi. No physical proximity is required. | +### `TOUCH` ---- +Means that user have to touch the back of the phone to unlock the reader. -## Example +### `NEARBY` -```kotlin -// Use the credential's default proximity (first in supportedUnlockProximities) -SeamSDK.getInstance().unlock(credentialId = credential.id!!) - -// Require tap-to-unlock -SeamSDK.getInstance().unlock( - credentialId = credential.id!!, - unlockProximity = UnlockProximity.TOUCH -) - -// Require nearby Bluetooth proximity -SeamSDK.getInstance().unlock( - credentialId = credential.id!!, - unlockProximity = UnlockProximity.NEARBY -) - -// Unlock remotely -SeamSDK.getInstance().unlock( - credentialId = credential.id!!, - unlockProximity = UnlockProximity.REMOTE -) -``` +Means that user have to be close to the reader to unlock it, but not necessarily touch. + +### `REMOTE` -**See also:** [`SeamCredential.supportedUnlockProximities`](/mobile-sdks/android/reference/seam-credential), [`SeamSDK.unlock()`](/mobile-sdks/android/reference/seam-sdk) +The reader is unlocked from a remote location, with wi-fi, for example. From acc6bc63997d1604b43434eb340649337c5db19d Mon Sep 17 00:00:00 2001 From: Seam Bot Date: Thu, 18 Jun 2026 19:57:51 +0000 Subject: [PATCH 2/2] ci: Format code --- .../android-reference/generate.ts | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/mintlify-codegen/android-reference/generate.ts b/mintlify-codegen/android-reference/generate.ts index b865b081d..a8c8b6129 100644 --- a/mintlify-codegen/android-reference/generate.ts +++ b/mintlify-codegen/android-reference/generate.ts @@ -189,12 +189,14 @@ function parseKdoc(raw: string | null): Kdoc { bodyLines.push(line) } else if (cursor.kind === 'param') { const p = params[cursor.idx] - if (p != null && line.trim() !== '') - {p.desc = `${p.desc} ${line.trim()}`.trim()} + if (p != null && line.trim() !== '') { + p.desc = `${p.desc} ${line.trim()}`.trim() + } } else if (cursor.kind === 'throws') { const t = throwsList[cursor.idx] - if (t != null && line.trim() !== '') - {t.desc = `${t.desc} ${line.trim()}`.trim()} + if (t != null && line.trim() !== '') { + t.desc = `${t.desc} ${line.trim()}`.trim() + } } else if (cursor.kind === 'returns') { if (line.trim() !== '') returns = `${returns} ${line.trim()}`.trim() } @@ -383,8 +385,7 @@ function sliceType(text: string, typeName: string): TypeSlice { if (kind === 'data class' && ctorParamsRaw != null) { const props = parseConstructorProps(ctorParamsRaw) const body = props.map((p) => ` ${p.signature}`).join(',\n') - declaration = - `${modifiers} ${keyword} ${typeName}(\n${body}\n)`.trim() + declaration = `${modifiers} ${keyword} ${typeName}(\n${body}\n)`.trim() } else { const ctor = ctorParamsRaw != null @@ -477,12 +478,13 @@ function extractBodyMembers(body: string, parentKind: TypeKind): MemberGroup[] { const name = fn[2] ?? '' const sig = extractFunctionSignature(code) if (isCompanion) companion.push({ name, signature: sig, doc }) - else - {funcs.push({ + else { + funcs.push({ name: `${isSuspend ? 'suspend ' : ''}${name}`, signature: sig, doc, - })} + }) + } } else if (nested != null) { const name = nested[3] ?? '' cases.push({ name, signature: extractNestedSignature(code), doc }) @@ -507,14 +509,16 @@ function extractBodyMembers(body: string, parentKind: TypeKind): MemberGroup[] { } const groups: MemberGroup[] = [] - if (companion.length > 0) - {groups.push({ title: 'Companion object methods', members: companion })} + if (companion.length > 0) { + groups.push({ title: 'Companion object methods', members: companion }) + } if (props.length > 0) groups.push({ title: 'Properties', members: props }) - if (cases.length > 0) - {groups.push({ + if (cases.length > 0) { + groups.push({ title: parentKind === 'enum class' ? 'Values' : 'Cases', members: cases, - })} + }) + } if (funcs.length > 0) groups.push({ title: 'Methods', members: funcs }) return groups } @@ -617,8 +621,9 @@ function renderMember(m: Member): string[] { if (m.doc.returns) lines.push('', `**Returns** — ${m.doc.returns}`) if (m.doc.throwsList.length > 0) { lines.push('', '**Throws**', '') - for (const t of m.doc.throwsList) - {lines.push(`- \`${t.type}\`${t.desc ? ` — ${t.desc}` : ''}`)} + for (const t of m.doc.throwsList) { + lines.push(`- \`${t.type}\`${t.desc ? ` — ${t.desc}` : ''}`) + } } lines.push('') return lines