Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions .github/workflows/blog-prose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,26 @@ jobs:
echo "status=$?" >> "$GITHUB_OUTPUT"
set -e

- name: Set up Node 20 for Mermaid validation
if: steps.detect.outputs.has_posts == 'true'
uses: actions/setup-node@v6
with:
node-version: '20'

- name: Install Playwright Chromium
if: steps.detect.outputs.has_posts == 'true'
run: |
set -euo pipefail
cd scripts
npm init -y 2>/dev/null || true
npm install playwright
npx playwright install-deps chromium
npx playwright install chromium

- name: Validate blog Mermaid diagrams
if: steps.detect.outputs.has_posts == 'true'
run: node scripts/website/validate_mermaid.mjs docs/website/content/blog

- name: Upload net-new findings report
if: ${{ always() && steps.detect.outputs.has_posts == 'true' }}
uses: actions/upload-artifact@v7
Expand Down
48 changes: 48 additions & 0 deletions docs/website/assets/css/extended/cn1-blog-post.css
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,46 @@ body.dark .cn1-blog-post .post-content table td {
border-color: rgba(175, 193, 255, 0.25);
}

.cn1-blog-post .blog-series-nav {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin: 30px 0 22px;
}

.cn1-blog-post .blog-series-nav a {
display: block;
min-height: 54px;
padding: 12px 14px;
border: 1px solid rgba(28, 81, 255, 0.18);
border-radius: 8px;
background: rgba(28, 81, 255, 0.04);
color: var(--primary);
font-size: 14px;
font-weight: 600;
line-height: 1.35;
}

.cn1-blog-post .blog-series-nav a:hover {
border-color: rgba(28, 81, 255, 0.36);
background: rgba(28, 81, 255, 0.08);
text-decoration: none;
}

.cn1-blog-post .blog-series-nav__next {
text-align: right;
}

body.dark .cn1-blog-post .blog-series-nav a {
border-color: rgba(175, 193, 255, 0.24);
background: rgba(175, 193, 255, 0.08);
}

body.dark .cn1-blog-post .blog-series-nav a:hover {
border-color: rgba(175, 193, 255, 0.42);
background: rgba(175, 193, 255, 0.12);
}

@media (max-width: 900px) {
.cn1-blog-post .post-content {
font-size: 16px;
Expand All @@ -96,4 +136,12 @@ body.dark .cn1-blog-post .post-content table td {
.cn1-blog-post .post-content h4 {
font-size: 19px;
}

.cn1-blog-post .blog-series-nav {
grid-template-columns: 1fr;
}

.cn1-blog-post .blog-series-nav__next {
text-align: left;
}
}
142 changes: 142 additions & 0 deletions docs/website/content/blog/carplay-android-auto-codename-one.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
title: "CarPlay And Android Auto From One Codename One API"
slug: carplay-android-auto-codename-one
url: /blog/carplay-android-auto-codename-one/
date: '2026-07-04'
author: Shai Almog
description: Codename One now projects driver-safe app screens to Apple CarPlay and Google Android Auto through one portable template API, with simulator support and zero native wiring when unused.
feed_html: '<img src="https://www.codenameone.com/blog/carplay-android-auto-codename-one.jpg" alt="CarPlay And Android Auto" /> Codename One now projects driver-safe app screens to Apple CarPlay and Google Android Auto through one portable template API.'
series: ["release-2026-07-03"]
---

![CarPlay And Android Auto From One Codename One API](/blog/carplay-android-auto-codename-one.jpg)

Yesterday's release post was about the bigger business line we will not cross: no royalties on IAP, ads, commerce or app revenue. This post is about one of the most concrete platform additions in that release. [PR #5281](https://github.com/codenameone/CodenameOne/pull/5281) adds Apple CarPlay and Google Android Auto support under `com.codename1.car`.

The first thing to know is what this is not. CarPlay and Android Auto are not second screens where your normal Codename One `Form` is drawn. They are driver-safe, template-based systems. Apple and Google decide which templates are legal in a car, how many rows can appear, which app categories are allowed, and which interactions are safe while driving.

Codename One now gives you one portable way to describe those templates.

![A CarPlay list template generated from the portable car model](/blog/carplay-android-auto-codename-one/carplay-list.png)

![An Android Auto grid template generated from the same portable car model](/blog/carplay-android-auto-codename-one/android-auto-grid.png)

## The Template Model

A car app registers a `CarApplication`. When a head unit connects, the application returns a root `CarScreen`. Each screen returns one `CarTemplate`: list, grid, pane, message, navigation, or now-playing.

```java
import com.codename1.car.*;

public class MyApp {
public void init(Object context) {
Car.setApplication(new MyCarApplication());
}
}

class MyCarApplication extends CarApplication {
public CarScreen onCreateRootScreen(CarContext context) {
return new LibraryScreen();
}
}

class LibraryScreen extends CarScreen {
protected CarTemplate onCreateTemplate() {
return new CarListTemplate().setTitle("Library")
.addRow(new CarRow("Now Playing")
.setText("Daft Punk - Discovery")
.setOnAction(ctx -> play()))
.addRow(new CarRow("Albums")
.setBrowsable(true)
.setOnAction(ctx -> ctx.pushScreen(new AlbumsScreen())))
.addRow(new CarRow("Playlists")
.setBrowsable(true)
.setOnAction(ctx -> ctx.pushScreen(new PlaylistsScreen())));
}
}
```

That code maps to `CPListTemplate` on CarPlay and `ListTemplate` on Android Auto. Grid items map to `CPGridButton` and `GridItem`. The now-playing template routes to the system media surface. Navigation gets a template shell with controls and ETA strips; the moving map surface is scaffolded but still being completed.

## Navigation And Lifecycle

`CarContext` gives you a head-unit stack:

```java
context.pushScreen(new DetailScreen());
context.popScreen();
screen.invalidate();
context.showToast("Added to queue");
```

`CarScreen` has lifecycle hooks for create, resume, pause, and destroy. `CarApplication` receives connect and disconnect callbacks, and you can listen globally:

```java
Car.addConnectionListener(new CarConnectionListener() {
public void carConnected(CarContext ctx) {
startLocationStream();
}

public void carDisconnected() {
stopLocationStream();
}
});
```

That is a good place to start and stop work that only exists while the dashboard is connected.

## Zero Cost When Unused

The build system scans your bytecode. If it sees `com.codename1.car`, it injects the native wiring. If it does not, the app carries none of this.

{{< mermaid >}}
flowchart TD
A["Your app bytecode"] --> B{"References com.codename1.car?"}
B -->|no| C["Normal iOS / Android build<br/>no car dependencies"]
B -->|yes| D["Inject car support"]
D --> E["iOS: CarPlay scene<br/>framework + entitlement"]
D --> F["Android: CarAppService<br/>androidx.car.app dependency"]
E --> G["Native head unit templates"]
F --> G
{{< /mermaid >}}

That is especially important for a framework with one codebase. A normal shopping app, game, or business tool should not gain a CarPlay dependency just because the framework knows how to build one.

## Build Hints And Approval

Car app categories matter. The build can inject wiring, but Apple and Google still decide whether an app is allowed into the car.

```properties
ios.carplay.audio=true
ios.carplay.navigation=true
android.androidAuto.navigation=true
android.androidAuto.poi=true
```

Audio is the default CarPlay category when no iOS car category is specified. Messaging, navigation and point-of-interest categories need the matching hints. Restricted Android Auto categories and real CarPlay deployment still require platform approval and, for iOS, the relevant CarPlay entitlement on your Apple App ID.

That is not a Codename One limitation. It is the safety model of the car platforms.

## Simulator Head Units

The simulator has a `Car` menu now. `Car > Connect CarPlay` and `Car > Connect Android Auto` open a simulated head-unit window that renders the same portable template tree. It is not a pixel-perfect emulator, but it gives you a fast loop for stack navigation, row caps and action callbacks.

![The Codename One simulator rendering a CarPlay-style list head unit](/blog/carplay-android-auto-codename-one/car-sim-list.png)

Selecting a browsable row pushes the next screen:

![The Codename One simulator rendering a CarPlay-style grid head unit](/blog/carplay-android-auto-codename-one/car-sim-grid.png)

That is the right level of simulation. You can build the car experience while still working in the normal desktop loop, then use Apple's CarPlay simulator or Android's Desktop Head Unit when you need to test the native projection layer.

## Wrapping Up

CarPlay and Android Auto support is a platform capability, not a revenue feature. There is no extra royalty because your podcast app, navigation app, or point-of-interest app reaches a dashboard. You still need Apple and Google approval for the categories they restrict, and you still need to design within the car template catalogue. Codename One now gives you the portable model and build wiring so that work lives in one codebase.

---

## Discussion

_Join the conversation via GitHub Discussions._

{{< giscus >}}
150 changes: 150 additions & 0 deletions docs/website/content/blog/commerce-secrets-without-iap-tax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
---
title: "Commerce And Secrets Without An IAP Tax"
slug: commerce-secrets-without-iap-tax
url: /blog/commerce-secrets-without-iap-tax/
date: '2026-07-06'
author: Shai Almog
description: Commerce validates receipts and normalizes entitlements, while Secrets keeps credentials out of the binary. Both are designed to fail soft, stay optional, and avoid turning IAP into a Codename One royalty.
feed_html: '<img src="https://www.codenameone.com/blog/commerce-secrets-without-iap-tax.jpg" alt="Commerce And Secrets Without An IAP Tax" /> Commerce validates receipts and normalizes entitlements, while Secrets keeps credentials out of the binary without adding a Codename One royalty to IAP.'
series: ["release-2026-07-03"]
---

![Commerce And Secrets Without An IAP Tax](/blog/commerce-secrets-without-iap-tax.jpg)

Commerce is the easiest feature in this release to misunderstand, so the first sentence has to be blunt:

**Commerce does not replace IAP and never will.**

Purchases still go through Apple, Google, or the payment processor you chose. Codename One does not process the payment, does not touch the money, and does not take a percentage. [PR #5300](https://github.com/codenameone/CodenameOne/pull/5300) adds infrastructure around the annoying backend work that comes after a purchase: validation, entitlement checks, subscription lifecycle, webhooks, and reporting.

That backend work is real. Anyone who has shipped subscriptions knows the trap. Buying a SKU is not the same as knowing whether the user has the right to a feature right now. Renewals, grace periods, refunds, billing retry, product changes, trials, family sharing and store server notifications all show up later. The device has one view. The store has another. Your backend usually needs a third.

Commerce is the optional service that turns that mess into an entitlement.

![Commerce dashboard for receipt validation, entitlements and revenue metrics](/blog/commerce-secrets-without-iap-tax/commerce.png)

{{< mermaid >}}
flowchart TD
A["App calls CommerceManager.subscribe()"] --> B["Purchase API"]
B --> C["Apple / Google store flow"]
C --> D["Store receipt"]
D --> E["Commerce refresh()"]
E --> F["Cloud receipt validation"]
F --> G["Entitlement cache"]
G --> H["isEntitled(pro)"]
C --> StoreOK["Purchase still completes even if cloud validation is unavailable"]
{{< /mermaid >}}

## Entitlements Instead Of SKU Branches

Your app should not need to know every SKU that grants `pro`. It should ask for `pro`.

```java
CommerceManager cm = CommerceManager.getInstance();
cm.setAppUserId(accountId);

if (cm.isEntitled("pro")) {
unlockProFeatures();
}
```

Purchases are still delegated to the existing `Purchase` API:

```java
cm.subscribe("pro_monthly");
// or
cm.purchase("remove_ads");
```

After a purchase, or when the app starts, refresh off the EDT:

```java
new Thread(() -> {
CommerceManager cm = CommerceManager.getInstance();
cm.refresh();

CN.callSerially(() -> {
if (cm.isEntitled("pro")) {
unlockProFeatures();
}
});
}).start();
```

`refresh()` validates the current receipts with the cloud when the build has a `build_key` and commerce is enabled. In a local build or simulator, it safely falls back to the normal `Purchase` path.

## What Happens When Quota Runs Out

This is the real question that matters most. If Commerce is tiered, what happens when a developer exceeds quota?

Validation degrades. Purchases do not stop.

`CommerceManager.isDegraded()` tells you the cloud did not return a server-validated answer. In that state, entitlement checks fall back to the platform's own receipt signal, treating the entitlement id as a subscription SKU when no cached cloud answer exists. That is less rich than server-side validation, but it is the right failure mode. A paying user should not be locked out because your account hit a validated-volume cap.

```java
cm.refresh();
if (cm.isDegraded()) {
Log.p("Commerce validation degraded; using store-direct fallback");
}
```

Commerce is tiered because it is a backend service that can be abused: receipt validation, store API calls, lifecycle processing, webhook delivery and revenue analytics cost real infrastructure. The degradation rule is what keeps that business reality from becoming user pain.

## What Commerce Adds

The service can:

- Validate receipts against Apple and Google.
- Normalize subscription state across stores.
- Track entitlements by your app user id.
- Forward lifecycle webhooks to your backend with HMAC signatures.
- Present revenue metrics such as MRR, ARR, ARPU, churn, trial conversion, cohort retention and realized LTV.

The app-facing API remains small because the complicated part lives server-side:

```java
CommerceManager cm = CommerceManager.getInstance();
cm.setAppUserId(myAccountId);
cm.refresh();
boolean active = cm.isEntitled("remove_ads");
```

That split explains how Commerce complements IAP instead of replacing it. `Purchase` starts the transaction. Commerce answers the longer-term entitlement question.

## Secrets

The same PR adds `com.codename1.security.Secrets`, which solves a different but related problem: API keys do not belong in source code or in the app binary.

![Secrets dashboard for managing app-readable secrets and server-side credentials](/blog/commerce-secrets-without-iap-tax/secrets.png)

```java
// Run off the EDT; the first call may hit the network.
String mapsKey = Secrets.get("googlemaps.key");
```

The value is fetched from the Codename One Cloud vault over TLS and cached in `SecureStorage`. `refresh(name)` forces a fresh fetch after you rotate the value server-side, and `clear(name)` drops the cached copy.

```java
String key = Secrets.refresh("googlemaps.key");
Secrets.clear("googlemaps.key");
```

Only app-readable secrets are served to the device. Server-only credentials, such as App Store Connect keys or Google Play service account JSON used for commerce validation, stay in the vault and are never reachable from client code.

That rule is non-negotiable: do not check API keys into source, do not paste them into snippets, and do not embed server credentials in the binary. If the app can read a secret, a determined attacker can eventually extract it. Secrets reduces exposure and makes rotation easier; it does not turn a client app into a secure server.

## The Boundary

Commerce and Secrets are both cloud features, but they sit on different sides of the volume line. Secrets usage stays low enough to enable it for everyone. Commerce has tiers because validation and analytics can create real backend load.

The important boundary is not tiering. The boundary is lock-in. You can still use the raw `Purchase` API. You can still build your own receipt backend. You can still ship an app that sells subscriptions without giving Codename One a revenue share.

Commerce is there to remove backend pain, not to insert a toll booth.

---

## Discussion

_Join the conversation via GitHub Discussions._

{{< giscus >}}
Loading
Loading