Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
cc99542
Add referral programme and promo-code client.
JonnyLuca Aug 29, 2026
7abfb08
Cover shareReferralInviteOrigin fallback when the box is empty or mis…
JonnyLuca Aug 29, 2026
9bdafc5
Cover ReferralCreatedInviteDto.fromJson when code is missing or blank.
JonnyLuca Aug 29, 2026
74dc4c9
Cover ReferralEntryCard tap through to the referral route.
JonnyLuca Aug 29, 2026
d3044de
Cover ReferralEligibilityCubit.reload timeout and initial-failure paths.
JonnyLuca Aug 29, 2026
30eee08
Cover ReferralCodeLookupDto.campaignTextLang when there is no copy.
JonnyLuca Aug 29, 2026
fde5bf5
Cover ReferralBindResultDto.campaignTextLang when there is no copy.
JonnyLuca Aug 29, 2026
93ac373
Cover ReferralInviteDto EN copy text and missing-code throw.
JonnyLuca Aug 29, 2026
502cd00
Cover localizedReferralError tokens for eligibility, terms, and bind.
JonnyLuca Aug 29, 2026
c659c1f
Cover ReferralCopyInviteButton unmount during an in-flight clipboard …
JonnyLuca Aug 29, 2026
f26de28
Cover ReferralPage load and terms-accepting create-invite listener.
JonnyLuca Aug 29, 2026
668a46e
Cover parseReferralDecimal mixed thousands and decimal separators.
JonnyLuca Aug 29, 2026
c3f3f38
Cover RealUnitReferralService ApiException throws on non-success HTTP…
JonnyLuca Aug 29, 2026
7fbe63f
Cover scheduleReferralBind, deferred unavailable Retry, and restash p…
JonnyLuca Aug 29, 2026
5ff7ce7
Cover ReferralCubit non-ApiException catch paths after accept, create…
JonnyLuca Aug 29, 2026
fff804d
Cover ReferralTermsPage SettingsBloc language and in-app terms link t…
JonnyLuca Aug 29, 2026
d35907c
Cover ReferralCreatePage load, overview submit, and in-flight create …
JonnyLuca Aug 29, 2026
dddebe1
Cover ReferralOverviewPage invite-list loading and create CTA open lock.
JonnyLuca Aug 29, 2026
3fff055
Cover ReferralOverviewPage non-const constructor for suite lcov.
JonnyLuca Aug 29, 2026
a41dd96
Cover ReferralCodeField stash races, clipboard, GetIt lookup, and pro…
JonnyLuca Aug 29, 2026
782942d
Cover remaining normalize_referral_code paste wrappers and HTML entit…
JonnyLuca Aug 29, 2026
4adf5dd
Add integration_test for the referral Einstiegskarte and macos runner.
JonnyLuca Aug 29, 2026
01cd894
Add integration_test for the referral terms checkbox gate.
JonnyLuca Aug 29, 2026
3bef7f6
Add integration_test for creating a named referral invite.
JonnyLuca Aug 29, 2026
0816726
Add integration_test for copying and sharing a referral invite.
JonnyLuca Aug 29, 2026
e200d48
Add integration_test for the referral overview counts and share price.
JonnyLuca Aug 29, 2026
8e9b5b4
Add integration_test for the referral prize in transaction history.
JonnyLuca Aug 29, 2026
b5ab3d0
Add integration_test for the registration invite and promo code step.
JonnyLuca Aug 29, 2026
cf108d0
Add integration_test for invite and promo App Link deeplinks.
JonnyLuca Aug 29, 2026
b627479
Add integration_test for invalid code, not-eligible, and prize-limit …
JonnyLuca Aug 29, 2026
ab11550
Add KYC referral golden for the invalid-code lookup error.
JonnyLuca Aug 29, 2026
2b29931
Hide ReferralEntryCard when the referral service is not registered.
JonnyLuca Aug 29, 2026
06e3ab2
test(goldens): regenerate baselines on the self-hosted runner
github-actions[bot] Aug 30, 2026
fa5402f
Add referral overview golden for the empty invite list.
JonnyLuca Aug 30, 2026
3598932
test(goldens): regenerate baselines on the self-hosted runner
github-actions[bot] Aug 30, 2026
a2f7818
Add remaining referral and promo screen-state goldens.
JonnyLuca Aug 30, 2026
cedda3b
Clear the KYC referral loading golden lookup timeout after the frame.
JonnyLuca Aug 30, 2026
0c4bf1b
test(goldens): regenerate baselines on the self-hosted runner
github-actions[bot] Aug 30, 2026
2bb1f8c
Fix referral entry integration tap on macOS
JonnyLuca Aug 30, 2026
c17100e
Set the macOS integration test view dimensions
JonnyLuca Aug 30, 2026
363d03b
Commit iOS and macOS build migrations
JonnyLuca Aug 30, 2026
02b9e70
Stabilize pointer E2E tests on headless macOS
JonnyLuca Aug 30, 2026
007349a
Pin referral lookup name fields
JonnyLuca Aug 30, 2026
a250428
Pin localized referral invite responses
JonnyLuca Aug 30, 2026
47ce214
Guard visible referral invite states
JonnyLuca Aug 30, 2026
263722c
Pin referral summary count contract
JonnyLuca Aug 30, 2026
18711cb
Pin referral running valuation contract
JonnyLuca Aug 30, 2026
53dbf27
Pin live referral terms contract
JonnyLuca Aug 30, 2026
29de89c
Format referral terms contract test
JonnyLuca Aug 30, 2026
b8fd88a
Send accepted referral terms version
JonnyLuca Aug 30, 2026
6a2a437
Format referral terms version contract
JonnyLuca Aug 30, 2026
f77626e
Align invite creation request contract
JonnyLuca Aug 30, 2026
4a012b8
Restore iOS framework deployment target
JonnyLuca Aug 30, 2026
e822743
fix(referral): correctness fixes from review (part 1)
JonnyLuca Sep 4, 2026
00a955b
fix(referral): conformity fixes from review (part 2)
JonnyLuca Sep 4, 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
8 changes: 4 additions & 4 deletions .github/workflows/handbook-build-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ jobs:
set -euo pipefail
bash scripts/assemble-handbook-screenshots.sh /tmp/handbook-shots
count=$(ls -1 /tmp/handbook-shots/*.png | wc -l | tr -d ' ')
if [ "$count" != "284" ]; then
echo "expected 284 screenshots, got $count" >&2
if [ "$count" != "299" ]; then
echo "expected 299 screenshots, got $count" >&2
exit 1
fi

Expand Down Expand Up @@ -173,11 +173,11 @@ jobs:
exit 1
fi

# Screenshots dir must contain all 284 PNGs assembled from Goldens.
# Screenshots dir must contain all 299 PNGs assembled from Goldens.
# Hit one of them through the auth gate to verify wiring end-to-end.
# Mix of the original 01-61 range and the 62-269 batch (every Golden
# baseline) so a regression in either half surfaces here.
for name in 01-welcome 11-dashboard 26-terms 35-dashboard-with-balance 46-buy-kyc-required 52-sell-unknown-error 53-buy-payment-details 61-kyc-registration-tax-tin-error 62-welcome-page-android 219-settings-security-page-default 268-phone-number-field-default 269-dashboard-insider-unlocked 270-handbook-persona-dca 275-handbook-persona-exit-1j; do
for name in 01-welcome 11-dashboard 26-terms 35-dashboard-with-balance 46-buy-kyc-required 52-sell-unknown-error 53-buy-payment-details 61-kyc-registration-tax-tin-error 62-welcome-page-android 219-settings-security-page-default 268-phone-number-field-default 269-dashboard-insider-unlocked 270-handbook-persona-dca 275-handbook-persona-exit-1j 276-referral-overview 282-referral-payout-row 283-referral-terms-readonly 287-kyc-registration-referral-recognized 288-transaction-history-referral-payout 289-dashboard-referral-entry-and-payout 290-kyc-registration-referral-promo; do
code=$(curl -s -o /dev/null -w '%{http_code}' -u "${HANDBOOK_USER:-x}:${HANDBOOK_PASS:-x}" "http://127.0.0.1:8080/screenshots/${name}.png")
# 200 (auth happens to match) or 401 (auth fails but file exists)
# both prove the file is on disk. 404 means it was not assembled.
Expand Down
6 changes: 6 additions & 0 deletions .maestro/handbook/11-dashboard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ appId: swiss.realunit.app
- extendedWaitUntil:
visible: 'RealUnit kaufen'
timeout: 30000
# Fresh handbook wallet is not a verified shareholder with 70 REALU
# (TB Ziff. 2, analog Verkaufen). The dashboard card must stay off this
# screen; 289-dashboard-referral-entry-and-payout is golden-only.
- extendedWaitUntil:
notVisible: 'Erhalte 20 REALU pro Weiterempfehlung'
timeout: 10000
10 changes: 10 additions & 0 deletions .maestro/handbook/12-settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,13 @@ appId: swiss.realunit.app
- extendedWaitUntil:
visible: 'Einstellungen'
timeout: 30000
# Same gate as the dashboard card. Wait for a settings tile that does not
# depend on the referral API, then assert Empfehlungen is still absent so
# a late summary 200 cannot have opened the tile on a fresh wallet.
- extendedWaitUntil:
visible:
text: '.*Rechtsdokumente.*'
timeout: 15000
- extendedWaitUntil:
notVisible: 'Empfehlungen'
timeout: 8000
17 changes: 17 additions & 0 deletions .maestro/handbook/20-settings-legal-documents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,20 @@ appId: swiss.realunit.app
visible:
text: '.*Dokumente der Aktionariat AG.*'
timeout: 30000
# Last tile is the 14.08 Teilnahmebedingungen (Ziff. 2–11). On a phone it can
# sit below the fold, so scroll it into view, then restore Aktionariat so
# flow 21 can tap that entry.
- scrollUntilVisible:
element:
text: '.*Teilnahmebedingungen Referral-Programm.*'
direction: DOWN
timeout: 15000
- extendedWaitUntil:
visible:
text: '.*Teilnahmebedingungen Referral-Programm.*'
timeout: 10000
- scrollUntilVisible:
element:
text: '.*Dokumente der Aktionariat AG.*'
direction: UP
timeout: 15000
6 changes: 6 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ linter:
avoid_print: true
prefer_const_constructors: true
prefer_single_quotes: true
analyzer:
exclude:
- build/**
- android/**
- ios/**
- macos/**
6 changes: 5 additions & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ if (keystorePropertiesFile.exists()) {
android {
namespace = "swiss.realunit.app"
compileSdk = 36
ndkVersion = "29.0.13113456 rc1"
ndkVersion = "29.0.13113456"

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
Expand Down Expand Up @@ -64,3 +64,7 @@ android {
flutter {
source = "../.."
}

dependencies {
implementation "com.android.installreferrer:installreferrer:2.2"
}
5 changes: 4 additions & 1 deletion android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@

# SumSub SDK general keep rules
-keep class com.sumsub.** { *; }
-dontwarn com.sumsub.**
-dontwarn com.sumsub.**

# Play Install Referrer (invite code across a fresh Android install)
-keep class com.android.installreferrer.** { *; }
28 changes: 28 additions & 0 deletions android/app/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,32 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- Staging App Links. Kept out of the release autoVerify filter so a
missing https://dev.realunit.app/.well-known/assetlinks.json cannot
fail Play verification for realunit.app. -->
<application>
<activity android:name=".MainActivity">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:scheme="https"
android:host="dev.realunit.app"
android:pathPrefix="/invite/"/>
<data
android:scheme="https"
android:host="dev.realunit.app"
android:pathPrefix="/promo/"/>
<data
android:scheme="https"
android:host="dev.realunit.app"
android:path="/invite"/>
<data
android:scheme="https"
android:host="dev.realunit.app"
android:path="/promo"/>
</intent-filter>
</activity>
</application>
</manifest>
46 changes: 46 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,52 @@
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="realunit-wallet"/>
</intent-filter>
<!-- HTTPS App Links for invite and promo codes (verified against
Digital Asset Links). autoVerify checks every host in this
filter: a missing assetlinks.json on any of them fails the
whole filter, so release only lists realunit.app / www.
Trailing slash on pathPrefix is required: "/invite" as a
prefix would also match "/invitee". Exact android:path
covers bare /invite?code=. Custom-scheme filter above still
covers realunit-wallet://invite|promo/{code}. Staging
host lives in debug/profile manifests. -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:scheme="https"
android:host="realunit.app"
android:pathPrefix="/invite/"/>
<data
android:scheme="https"
android:host="realunit.app"
android:pathPrefix="/promo/"/>
<data
android:scheme="https"
android:host="realunit.app"
android:path="/invite"/>
<data
android:scheme="https"
android:host="realunit.app"
android:path="/promo"/>
<data
android:scheme="https"
android:host="www.realunit.app"
android:pathPrefix="/invite/"/>
<data
android:scheme="https"
android:host="www.realunit.app"
android:pathPrefix="/promo/"/>
<data
android:scheme="https"
android:host="www.realunit.app"
android:path="/invite"/>
<data
android:scheme="https"
android:host="www.realunit.app"
android:path="/promo"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
Expand Down
61 changes: 60 additions & 1 deletion android/app/src/main/kotlin/swiss/dfx/realunit/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,64 @@
package swiss.realunit.app

import android.os.Handler
import android.os.Looper
import com.android.installreferrer.api.InstallReferrerClient
import com.android.installreferrer.api.InstallReferrerStateListener
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterFragmentActivity()
class MainActivity : FlutterFragmentActivity() {
private val installReferrerChannel = "swiss.realunit.app/install_referrer"

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installReferrerChannel)
.setMethodCallHandler { call, result ->
if (call.method != "readInstallReferrer") {
result.notImplemented()
return@setMethodCallHandler
}
readInstallReferrer(result)
}
}

private fun readInstallReferrer(result: MethodChannel.Result) {
val client = InstallReferrerClient.newBuilder(this).build()
val main = Handler(Looper.getMainLooper())
var replied = false

fun reply(value: String?) {
if (replied) return
replied = true
result.success(value)
try {
client.endConnection()
} catch (_: Exception) {
}
}

main.postDelayed({ reply(null) }, 3000)
try {
client.startConnection(
object : InstallReferrerStateListener {
override fun onInstallReferrerSetupFinished(responseCode: Int) {
if (responseCode != InstallReferrerClient.InstallReferrerResponse.OK) {
reply(null)
return
}
try {
reply(client.installReferrer.installReferrer)
} catch (_: Exception) {
reply(null)
}
}

override fun onInstallReferrerServiceDisconnected() {}
},
)
} catch (_: Exception) {
reply(null)
}
}
}
25 changes: 25 additions & 0 deletions android/app/src/profile/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,29 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<application>
<activity android:name=".MainActivity">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:scheme="https"
android:host="dev.realunit.app"
android:pathPrefix="/invite/"/>
<data
android:scheme="https"
android:host="dev.realunit.app"
android:pathPrefix="/promo/"/>
<data
android:scheme="https"
android:host="dev.realunit.app"
android:path="/invite"/>
<data
android:scheme="https"
android:host="dev.realunit.app"
android:path="/promo"/>
</intent-filter>
</activity>
</application>
</manifest>
4 changes: 4 additions & 0 deletions android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
2 changes: 1 addition & 1 deletion android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
49 changes: 49 additions & 0 deletions assets/languages/strings_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,55 @@
"receiver": "Empfänger",
"recoveryWords": "Wiederherstellungs-Wörter",
"recoveryWordsInvalid": "Wiederherstellungs-Wörter ungültig",
"referralAlreadyBound": "Du hast bereits einen Einladungs- oder Promo-Code eingelöst.",
"referralAlreadyRegistered": "Diese Einladung gilt nur für neue Kundinnen und Kunden.",
"referralChfAtSharePrice": "≈ ${chf} zum ${label}",
"referralCodeChecking": "Code wird geprüft…",
"referralCodeDescription": "Optional. Über den Einladungslink wird der Code automatisch übernommen; hier kannst du ihn auch manuell eingeben.",
"referralCodeHeading": "Hast du einen Empfehlungscode?",
"referralCodeHint": "Einladungs- oder Promo-Code",
"referralCodeInvalid": "Dieser Code ist ungültig oder abgelaufen.",
"referralCodeInvalidTitle": "Link ungültig oder abgelaufen",
"referralCodeOptional": "Einladungs- oder Promo-Code (optional)",
"referralCodeSpent": "Dieser Einladungs- oder Promo-Code wurde bereits verwendet.",
"referralCodeSpentTitle": "Code bereits eingelöst",
"referralCodeUnavailable": "Wir konnten den Code gerade nicht prüfen. Bitte versuche es später erneut.",
"referralCopied": "Kopiert",
"referralCopyInviteLink": "Einladungslink kopieren",
"referralCreateInvite": "Einladungslink erstellen",
"referralCreateInviteDescription": "Gib den Vornamen der Person ein, die du einladen möchtest.",
"referralCreating": "Einladung wird erstellt…",
"referralGuestName": "Name der eingeladenen Person",
"referralGuestNameRequired": "Bitte den Vornamen der eingeladenen Person eingeben.",
"referralInviteRecognized": "Einladung von ${inviter} erkannt. ${inviter} erhält seine Prämie automatisch, sobald du verifiziert bist und deinen ersten Kauf abgeschlossen hast.",
"referralInvitesLoading": "Offene Einladungen werden geladen…",
"referralInviteUrlLabel": "Persönlicher Einladungslink",
"referralLoading": "Empfehlungen werden geladen…",
"referralNotEligible": "Das Empfehlungsprogramm steht verifizierten Aktionären mit dem erforderlichen Bestand zur Verfügung.",
"referralOpenInvitesExpire": "Offene Einladungen verfallen nach 3 Monaten.",
"referralOverviewHint": "20 REALU pro Weiterempfehlung, sobald der erste Kauf erfolgreich abgewickelt ist.",
"referralOverviewTitle": "Deine Empfehlungen",
"referralPayout": "Empfehlungsprämie",
"referralPayoutChf": "CHF ${chf} zum Zeitpunkt der Gutschrift",
"referralPromoTitle": "Aktion",
"referralQuarterCap": "In diesem Quartal sind keine weiteren Prämien möglich.",
"referrals": "Empfehlungen",
"referralSelfReferral": "Du kannst deine eigene Einladung nicht einlösen.",
"referralShareInviteLink": "Einladungslink versenden",
"referralSharePrice": "Aktienkurs",
"referralShareText": "Hey ${guestName}, ${hostName} lädt dich ein zu RealUnit: ${url}",
"referralShareTextNoName": "${hostName} lädt dich ein zu RealUnit: ${url}",
"referralsSubtitle": "Erhalte 20 REALU pro Weiterempfehlung",
"referralStatusCredited": "Gutgeschrieben",
"referralStatusOpen": "Offen",
"referralTermsAccepting": "Teilnahmebedingungen werden akzeptiert…",
"referralTermsCheckbox": "Ich habe die Teilnahmebedingungen gelesen und akzeptiert",
"referralTermsLoading": "Teilnahmebedingungen werden geladen…",
"referralTermsRequired": "Bitte zuerst die Teilnahmebedingungen akzeptieren.",
"referralTermsTitle": "Teilnahmebedingungen Referral-Programm",
"referralTotalReceived": "Insgesamt erhalten",
"referralYourInvite": "Deine Einladung",
"referralYourInviteFor": "Deine Einladung für ${guestName}",
"refresh": "Aktualisieren",
"registerAccountType": "Kontotyp",
"registerCitizenship": "Staatsangehörigkeit",
Expand Down
Loading