Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2d14b0c
Generate the iOS string catalog into the RespondKitUI SwiftPM target
lhr0909 Oct 15, 2026
81f1294
Test that the React Native store asks for email only after the contac…
lhr0909 Oct 15, 2026
b1b614f
React Native: hide the email ask until the identity's contact is known
lhr0909 Oct 15, 2026
3d264fa
Guard the web widget against an email-prompt flash
lhr0909 Oct 15, 2026
7a6c6c8
Test that the iOS store never asks for a supplied or stored email
lhr0909 Oct 15, 2026
e44a012
iOS: no email-ask flash; empty state, subtitle and attach menu from t…
lhr0909 Oct 15, 2026
50f2793
Test that the Android store never asks for a supplied or stored email
lhr0909 Oct 15, 2026
3c1a3ca
Android: paperclip attach button left of the input, no email-ask flas…
lhr0909 Oct 15, 2026
a0bbc4a
Release 0.6.2: no email-ask flash, Android attach icon, catalog strings
lhr0909 Oct 15, 2026
8e57e5f
Test /approve and the draft commands on an agent reply that was alrea…
lhr0909 Oct 15, 2026
0c128cd
Tell operators an agent reply was already sent instead of 'not an age…
lhr0909 Oct 15, 2026
9475ccc
0.6.2 notes: already-sent agent replies and the API deploy step
lhr0909 Oct 15, 2026
1de16ef
Revert the already-sent /approve answer (msg-1009) from this PR
lhr0909 Oct 15, 2026
9f27838
Compile the moved Swift catalog in the localization workflow
lhr0909 Oct 15, 2026
fb72a49
Test failed and canceled contact lookups on every store
lhr0909 Oct 15, 2026
a1d81ae
Resolve the contact only after a successful lookup, retrying on the c…
lhr0909 Oct 15, 2026
58b2b94
Generate a typed message module for web and React Native
lhr0909 Oct 15, 2026
82c5e47
Web and React Native: empty state, subtitle and attach menu from the …
lhr0909 Oct 15, 2026
c653a6c
Docs: JS message module, contact retry in the 0.6.2 notes
lhr0909 Oct 15, 2026
022029f
Test that an identity switch during a failed contact lookup uses noth…
lhr0909 Oct 15, 2026
1c7f181
Recheck the identity epoch after resolving the contact
lhr0909 Oct 15, 2026
1e58df7
Pin the React Native peers in the npm smoke install
lhr0909 Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/localization.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- "localization/**"
- "scripts/generate-localizations.ts"
- "scripts/test-localization-apple.swift"
- "native/swift/Sources/RespondKitUI/Resources/**"
- "package.json"
- "pnpm-lock.yaml"
- ".github/workflows/localization.yml"
Expand All @@ -28,7 +29,7 @@ jobs:
- if: runner.os == 'macOS'
run: |
mkdir -p "$RUNNER_TEMP/localization-apple"
xcrun xcstringstool compile localization/generated/ios/RespondKit.xcstrings --output-directory "$RUNNER_TEMP/localization-apple"
xcrun xcstringstool compile native/swift/Sources/RespondKitUI/Resources/RespondKit.xcstrings --output-directory "$RUNNER_TEMP/localization-apple"
swift scripts/test-localization-apple.swift "$RUNNER_TEMP/localization-apple"
- if: runner.os == 'Linux'
uses: actions/setup-java@v4
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PackageDescription

let package = Package(
name: "RespondKit",
defaultLocalization: "en",
platforms: [.iOS(.v18), .macOS(.v15)],
products: [
.library(name: "RespondKitCore", targets: ["RespondKitCore"]),
Expand All @@ -12,7 +13,7 @@ let package = Package(
.target(name: "RespondKitCore", path: "native/swift/Sources/RespondKitCore"),
.target(
name: "RespondKitUI", dependencies: ["RespondKitCore"],
path: "native/swift/Sources/RespondKitUI"),
path: "native/swift/Sources/RespondKitUI", resources: [.process("Resources")]),
.testTarget(
name: "RespondKitTests", dependencies: ["RespondKitCore", "RespondKitUI"], path: "native",
exclude: ["android", "examples", "swift/Sources"],
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.6.1
0.6.2
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@respondkit/api",
"version": "0.6.1",
"version": "0.6.2",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion apps/native-example/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@respondkit/native-example",
"version": "0.6.1",
"version": "0.6.2",
"private": true,
"scripts": {
"start": "react-native start",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@respondkit/web",
"version": "0.6.1",
"version": "0.6.2",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion apps/widget/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@respondkit/widget-playground",
"version": "0.6.1",
"version": "0.6.2",
"private": true,
"type": "module",
"scripts": {
Expand Down
23 changes: 23 additions & 0 deletions docs/releases/0.6.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# RespondKit 0.6.2

Status: prepared, not published. A merged PR does not publish SDK artifacts.

## Release notes

A client patch for three problems found in ReceiptGenie's iOS and Android apps with 0.6.1.

- **No email-ask flash.** The native stores and the React Native store now wait until the contact lookup for the current identity has succeeded before asking for an email (`asksForEmail`). A failed or interrupted lookup keeps the ask hidden and is retried with the next request on the same session. The ask never appears when the host supplied an email or the server already stores one. Before, iOS, Android and React Native showed the ask for a moment while the stored contact was being fetched. The web widget already waited, and new tests guard it.
- **Android attach button.** The Compose composer's text "Attach files" button above the input is now a paperclip icon button to the left of the message field. It matches the web and iOS composers.
- **Translated strings.** The SwiftPM `RespondKitUI` target and the Compose module now bundle the resources generated from `localization/messages`, and the web and React Native packages ship a generated `messages.ts`. The empty state, the "Ask us anything" subtitle and the attach menu read from that catalog on every client (web by `context.locale`, React Native by the device locale). Other strings remain English literals for now. The catalog ships English only. Other languages need a reviewed `messages/{locale}.json`.

## Upgrade and compatibility

Use `0.6.2` for the npm packages, `v0.6.2` for SwiftPM and `0.6.2` for Maven. No API or protocol change.

- `RespondKitStore.asksForEmail` (Swift, Kotlin state) and `SupportSnapshot.asksForEmail` (React Native) are new. Custom screens that show their own ask should use them instead of checking the email for null.
- Android: the Compose module's own `res/values/strings.xml` is gone, and its strings come from the shared catalog. `respondkit_new` no longer exists (use `respondkit_new_conversation`), and `respondkit_intro` now holds the longer intro line, with the short one in `respondkit_intro_short`. Hosts that override either name must update their overrides.

## Operator checklist

1. Publish GitHub release `v0.6.2`, which runs the npm publish workflow. Publish Maven Central per `docs/native/releases.md`.
2. Point ReceiptGenie 2.9.0 at 0.6.2.
10 changes: 5 additions & 5 deletions localization/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ pnpm localization:check

`project.inlang/settings.json` defines the base language and release locales. Add each new locale there and supply all message IDs in `messages/{locale}.json`. This is ordinary inlang message-format JSON, compatible with Paraglide JS. The native exporters are intentionally limited to the contract below. They do not load arbitrary inlang plugins.

The generator writes `generated/ios/RespondKit.xcstrings` and `generated/android/res/values*/strings.xml`. Commit the outputs with the JSON changes. `pnpm ready` fails if an output is missing, modified, or left behind after removing a locale. Generated files belong to this pipeline; edits in Xcode or Android Studio must be made in JSON instead. No translation services run during compilation.
The generator writes `native/swift/Sources/RespondKitUI/Resources/RespondKit.xcstrings` (the SwiftPM resource of `RespondKitUI`) `generated/android/res/values*/strings.xml` (a resource directory of the Compose module), and one TypeScript module copied to `packages/react/src/widget/messages.ts` and `packages/react-native/src/messages.ts`. Commit the outputs with the JSON changes. `pnpm ready` fails if an output is missing, modified, or left behind after removing a locale. Generated files belong to this pipeline; edits in Xcode or Android Studio must be made in JSON instead. No translation services run during compilation.

These are build tools and an English source catalog. The current SDK views still use their existing strings; the generated resources are not yet installed in the SDK bundles. Migrating lookups and adding UI locale selection are separate integration work. No new languages are advertised by the SDK in this change.
Every client reads the empty state, the "Ask us anything" subtitle and the attach menu from the generated output, and Compose also reads its closed-conversation strings from it. Other views still use English literals. The web widget picks its catalog from the host's `context.locale`, React Native from the device locale; both fall back to the closest shipped locale, then English. The catalog is English only, so adding a language means adding it to `settings.json` with a complete `messages/{locale}.json`.

## Portable message contract

Expand Down Expand Up @@ -55,9 +55,9 @@ The output contains patterns, not sentences pre-rendered for example counts. Xco

- `@respondkit/paraglide-ios`: `compileXCStrings(input)` returns the catalog JSON as a string.
- `@respondkit/paraglide-android`: `compileAndroidResources(input)` returns a map of relative resource paths to XML strings.
- `@respondkit/paraglide-resources`: shared validation, placeholder conversion, and plural expansion.
- `@respondkit/paraglide-resources`: shared validation, placeholder conversion, and plural expansion. `compileJavaScriptMessages(input)` returns the TypeScript module for web and React Native: `messages(locale)` gives typed functions per message ID.

These are private workspace packages for now. They build independently with `pnpm --filter @respondkit/paraglide-ios build` and the equivalent Android command. They are not published npm packages. Paraglide and its plugin are test-only dependencies; native compiler code has no network calls or Paraglide runtime dependency.
These are private workspace packages for now. They build independently with `pnpm --filter @respondkit/paraglide-ios build` and the equivalent Android command. They are not published npm packages. Paraglide and its plugin are test-only dependencies; the compilers have no network calls or Paraglide runtime dependency.

```ts
import { compileXCStrings } from "@respondkit/paraglide-ios";
Expand Down Expand Up @@ -90,7 +90,7 @@ stringResource(R.string.respondkit_remove_file, fileName)
pluralStringResource(R.plurals.respondkit_show_new, count, count)
```

Both counts are required in the last call: the first selects the plural category, the second fills the displayed number. React and React Native can consume the same JSON through Paraglide's JavaScript compiler; RN locale handling and Metro/Hermes integration still need separate validation.
Both counts are required in the last call: the first selects the plural category, the second fills the displayed number. Web and React Native use the generated TypeScript module rather than Paraglide's JavaScript compiler, which would load the inlang project and its CDN-hosted plugin at build time.

## Inspected translation surface

Expand Down
1 change: 1 addition & 0 deletions native/android/compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures { compose = true }
sourceSets["main"].res.srcDir("../../../localization/generated/android/res")
sourceSets["androidTest"].assets.srcDir("../../fixtures")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,38 @@ class RespondKitScreenTest {
compose.onNodeWithTag("respondkit-composer").assertTextContains("Preserved draft")
}

@Test
fun attachIconSitsLeftOfTheMessageFieldInTheSendRow() {
compose.setContent {
val scope = rememberCoroutineScope()
val store = remember {
RespondKitStore(
RespondKitConfiguration(
"https://support.example.com",
"inbox_test",
"https://example.com",
),
persistence = MemoryPersistence(),
api = Api(),
scope = scope,
)
}
RespondKitScreen(store, onClose = {})
}
val attach = compose.onNodeWithContentDescription("Attach files").assertHasClickAction()
.getUnclippedBoundsInRoot()
val field = compose.onNodeWithTag("respondkit-composer").getUnclippedBoundsInRoot()
val send = compose.onNodeWithTag("respondkit-send").getUnclippedBoundsInRoot()
org.junit.Assert.assertTrue("attach is left of the field", attach.right <= field.left)
org.junit.Assert.assertEquals(send.top, attach.top)
org.junit.Assert.assertEquals(send.bottom, attach.bottom)
org.junit.Assert.assertTrue("attach shares the field row", attach.top < field.bottom && attach.bottom > field.top)
compose.onNodeWithText("Attach files").assertDoesNotExist()
compose.onNodeWithContentDescription("Attach files").performClick()
compose.onNodeWithText("Photo library").assertExists()
compose.onNodeWithText("Files").assertExists()
}

@Test
fun screenInheritsHostAccentAndSupportsOverride() {
val inherited = Color(0xFF14B8A6)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import dev.respondkit.core.*
import java.io.File
Expand All @@ -23,7 +26,13 @@ private data class PickedFile(val id: String, val uri: Uri, val name: String, va
val type: String = "application/octet-stream", val attachment: SupportAttachment? = null, val error: String? = null)

@Composable
internal fun AttachmentPicker(store: RespondKitStore, clear: Int, isSending: Boolean, onChange: (List<SupportAttachment>, Boolean) -> Unit) {
internal fun AttachmentPicker(
store: RespondKitStore,
clear: Int,
isSending: Boolean,
onChange: (List<SupportAttachment>, Boolean) -> Unit,
content: @Composable (attachButton: @Composable () -> Unit) -> Unit,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var picked by remember(store.attachmentScope, clear) { mutableStateOf(emptyList<PickedFile>()) }
Expand Down Expand Up @@ -68,15 +77,6 @@ internal fun AttachmentPicker(store: RespondKitStore, clear: Int, isSending: Boo
val photos = rememberLauncherForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { if (requestedScope == store.attachmentScope) select(it); requestedScope = null }
val files = rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { if (requestedScope == store.attachmentScope) select(it); requestedScope = null }
Column(Modifier.fillMaxWidth()) {
Box {
TextButton(onClick = { expanded = true }, enabled = !isSending) { Text("Attach files") }
DropdownMenu(expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(text = { Text("Photo library") }, onClick = {
expanded = false; requestedScope = store.attachmentScope; photos.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo))
})
DropdownMenuItem(text = { Text("Files") }, onClick = { expanded = false; requestedScope = store.attachmentScope; files.launch(arrayOf("*/*")) })
}
}
Column(Modifier.heightIn(max = 128.dp).verticalScroll(rememberScrollState())) {
picked.forEach { item ->
Row(verticalAlignment = Alignment.CenterVertically) {
Expand All @@ -88,5 +88,24 @@ internal fun AttachmentPicker(store: RespondKitStore, clear: Int, isSending: Boo
if (item.error != null) Text(item.error, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
}
}
content {
Box {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
IconButton(
onClick = { expanded = true },
enabled = !isSending,
modifier = Modifier.size(44.dp).testTag("respondkit-attach"),
) {
Icon(painterResource(R.drawable.respondkit_attach), stringResource(R.string.respondkit_attach_files), Modifier.size(16.dp))
}
}
DropdownMenu(expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(text = { Text(stringResource(R.string.respondkit_photo_library)) }, onClick = {
expanded = false; requestedScope = store.attachmentScope; photos.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo))
})
DropdownMenuItem(text = { Text(stringResource(R.string.respondkit_files)) }, onClick = { expanded = false; requestedScope = store.attachmentScope; files.launch(arrayOf("*/*")) })
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ private fun RespondKitContent(
color = WidgetInk,
maxLines = 1,
)
Text("Ask us anything", fontSize = 14.sp, lineHeight = 20.sp, color = WidgetMuted)
Text(stringResource(R.string.respondkit_ask_anything), fontSize = 14.sp, lineHeight = 20.sp, color = WidgetMuted)
}
IconButton(
onClick = onClose,
Expand Down Expand Up @@ -270,13 +270,13 @@ private fun Conversation(store: RespondKitStore, state: SupportState, modifier:
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
"How can we help?",
stringResource(R.string.respondkit_help),
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = WidgetInk,
)
Text(
"Send a message and keep this page open for a quick reply.",
stringResource(R.string.respondkit_intro),
fontSize = 14.sp,
color = WidgetMuted,
textAlign = TextAlign.Center,
Expand Down Expand Up @@ -319,7 +319,7 @@ private fun Conversation(store: RespondKitStore, state: SupportState, modifier:
}
}
} else {
if (state.emailAddress == null) EmailCapture(store, state.isLoading)
if (state.asksForEmail) EmailCapture(store, state.isLoading)
Composer(store, state)
}
}
Expand Down Expand Up @@ -351,13 +351,13 @@ private fun Composer(store: RespondKitStore, state: SupportState) {
!state.isSending &&
(state.draft.isNotBlank() || attachments.isNotEmpty()) && !uploading &&
state.draft.length <= 6_000
Column {
AttachmentPicker(store, clearAttachments, state.isSending) { files, busy -> attachments = files; uploading = busy }
AttachmentPicker(store, clearAttachments, state.isSending, { files, busy -> attachments = files; uploading = busy }) { attachButton ->
Row(
Modifier.fillMaxWidth().padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.Bottom,
) {
attachButton()
BasicTextField(
state.draft,
store::setDraft,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:strokeColor="#FF000000" android:strokeWidth="2" android:strokeLineJoin="round" android:strokeLineCap="round" android:pathData="M16,6L7.586,14.586A2,2 0,0 0,10.415 17.415L18.829,8.829A4,4 0,1 0,13.172 3.172L4.793,11.723A6,6 0,1 0,13.278 20.208L21.657,11.657" />
</vector>
24 changes: 0 additions & 24 deletions native/android/compose/src/main/res/values/strings.xml

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ internal data class StoredState(

data class SupportState(
val emailAddress: String? = null,
/** True once the session's contact lookup found no email; false while it is unknown. */
val asksForEmail: Boolean = false,
val isFreshConversation: Boolean = false,
val statuses: List<ThreadStatus> = emptyList(),
val unreadThreadIds: Set<String> = emptySet(),
Expand Down
Loading
Loading