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
1 change: 1 addition & 0 deletions assets/languages/strings_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@
"street": "Strasse",
"supportBugReport": "Fehlerbericht",
"supportChat": "Support Chat",
"supportChatSupportLabel": "Support",
"supportCreateTicket": "Neues Ticket erstellen",
"supportCreateTicketDescription": "Erstellen Sie ein neues Support-Ticket",
"supportEnterMessage": "Nachricht eingeben",
Expand Down
1 change: 1 addition & 0 deletions assets/languages/strings_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@
"street": "Street",
"supportBugReport": "Bug report",
"supportChat": "Support chat",
"supportChatSupportLabel": "Support",
"supportCreateTicket": "Create new ticket",
"supportCreateTicketDescription": "Create a new support ticket",
"supportEnterMessage": "Enter message",
Expand Down
8 changes: 7 additions & 1 deletion lib/packages/service/dfx/models/support/support_message.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import 'package:equatable/equatable.dart';
import 'package:realunit_wallet/packages/service/dfx/models/support/dto/support_message_dto.dart';

// Mirrors the API constant in `support-message.entity.ts`. Customer-authored
// messages carry this exact author value; anything else (agent name,
// `AutoResponder`, …) is rendered as support.
const String customerAuthor = 'Customer';

class SupportMessage extends Equatable {
final int id;
final String? author;
Expand All @@ -16,7 +21,8 @@ class SupportMessage extends Equatable {
this.fileName,
});

bool get isFromSupport => author == null;
bool get isFromCustomer => author == customerAuthor;
bool get isFromSupport => !isFromCustomer;

factory SupportMessage.fromDto(SupportMessageDto dto) {
return SupportMessage(
Expand Down
78 changes: 48 additions & 30 deletions lib/screens/support/widgets/support_chat_message_bubble.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:realunit_wallet/generated/i18n.dart';
import 'package:realunit_wallet/packages/service/dfx/models/support/support_message.dart';
import 'package:realunit_wallet/styles/colors.dart';

Expand All @@ -12,44 +13,61 @@ class SupportChatMessageBubble extends StatelessWidget {

@override
Widget build(BuildContext context) {
final isFromUser = !supportMessage.isFromSupport;
final isFromCustomer = supportMessage.isFromCustomer;

return Padding(
padding: const .symmetric(vertical: 4),
child: Row(
mainAxisAlignment: isFromUser ? .end : .start,
mainAxisAlignment: isFromCustomer ? .end : .start,
children: [
Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.75,
),
padding: const .symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
color: isFromUser ? RealUnitColors.realUnitBlue : RealUnitColors.neutral100,
borderRadius: .circular(12),
),
child: Column(
crossAxisAlignment: isFromUser ? .end : .start,
spacing: 4.0,
children: [
if (supportMessage.message != null)
Text(
supportMessage.message!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: isFromUser ? RealUnitColors.basic.white : RealUnitColors.neutral900,
Column(
crossAxisAlignment: isFromCustomer ? .end : .start,
spacing: 2.0,
children: [
if (!isFromCustomer)
Padding(
padding: const .only(left: 4),
child: Text(
S.of(context).supportChatSupportLabel,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: RealUnitColors.neutral500,
fontWeight: FontWeight.w600,
),
),
Text(
_formatTime(supportMessage.created),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: isFromUser ? RealUnitColors.neutral300 : RealUnitColors.neutral500,
),
),
],
),
Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.75,
),
padding: const .symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
color: isFromCustomer ? RealUnitColors.realUnitBlue : RealUnitColors.neutral100,
borderRadius: .circular(12),
),
child: Column(
crossAxisAlignment: isFromCustomer ? .end : .start,
spacing: 4.0,
children: [
if (supportMessage.message != null)
Text(
supportMessage.message!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: isFromCustomer ? RealUnitColors.basic.white : RealUnitColors.neutral900,
),
),
Text(
_formatTime(supportMessage.created),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: isFromCustomer ? RealUnitColors.neutral300 : RealUnitColors.neutral500,
),
),
],
),
),
],
),
],
),
Expand Down
40 changes: 35 additions & 5 deletions test/packages/service/dfx/models/support/support_message_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,28 +54,58 @@ void main() {
expect(msg.fileName, dto.fileName);
});

test('isFromSupport is true when author is null', () {
// The API constant `CustomerAuthor` is the single value that marks a
// customer-authored message. Everything else (agent name, AutoResponder,
// legacy null) renders as support.
test('isFromCustomer is true for author == "Customer"', () {
final msg = SupportMessage.fromDto(
SupportMessageDto(
id: 1,
author: 'Customer',
created: DateTime.utc(2026, 1, 1),
// author absent → from support (server side)
),
);

expect(msg.isFromCustomer, isTrue);
expect(msg.isFromSupport, isFalse);
});

test('isFromSupport is true for an agent name', () {
final msg = SupportMessage.fromDto(
SupportMessageDto(
id: 1,
author: 'Robin',
created: DateTime.utc(2026, 1, 1),
),
);

expect(msg.isFromCustomer, isFalse);
expect(msg.isFromSupport, isTrue);
});

test('isFromSupport is false when author is set', () {
test('isFromSupport is true for the AutoResponder bot', () {
final msg = SupportMessage.fromDto(
SupportMessageDto(
id: 1,
author: 'alice',
author: 'AutoResponder',
created: DateTime.utc(2026, 1, 1),
),
);

expect(msg.isFromSupport, isFalse);
expect(msg.isFromCustomer, isFalse);
expect(msg.isFromSupport, isTrue);
});

test('isFromSupport is true when author is null (defensive)', () {
final msg = SupportMessage.fromDto(
SupportMessageDto(
id: 1,
created: DateTime.utc(2026, 1, 1),
),
);

expect(msg.isFromCustomer, isFalse);
expect(msg.isFromSupport, isTrue);
});
});

Expand Down
53 changes: 42 additions & 11 deletions test/screens/support/widgets/support_chat_message_bubble_test.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:realunit_wallet/generated/i18n.dart';
import 'package:realunit_wallet/packages/service/dfx/models/support/support_message.dart';
import 'package:realunit_wallet/screens/support/widgets/support_chat_message_bubble.dart';
import 'package:realunit_wallet/styles/colors.dart';

Widget _host(Widget child) => MaterialApp(home: Scaffold(body: child));
Widget _host(Widget child) => MaterialApp(
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: Scaffold(body: child),
);

SupportMessage _msg({
String? author,
Expand All @@ -20,24 +29,26 @@ SupportMessage _msg({

void main() {
group('$SupportChatMessageBubble', () {
testWidgets('user message: blue bubble, aligned to end', (tester) async {
testWidgets('customer message: blue bubble end-aligned, no support label',
(tester) async {
await tester.pumpWidget(_host(
SupportChatMessageBubble(supportMessage: _msg(author: 'user-1')),
SupportChatMessageBubble(supportMessage: _msg(author: 'Customer')),
));

final container = tester.widget<Container>(find.byType(Container));
final decoration = container.decoration! as BoxDecoration;
expect(decoration.color, RealUnitColors.realUnitBlue);

// The wrapping Row aligns to MainAxisAlignment.end for the user bubble.
final row = tester.widget<Row>(find.byType(Row));
expect(row.mainAxisAlignment, MainAxisAlignment.end);

expect(find.text('Support'), findsNothing);
});

testWidgets('support message: neutral grey bubble, aligned to start',
testWidgets('support agent message: grey bubble start-aligned, with label',
(tester) async {
await tester.pumpWidget(_host(
SupportChatMessageBubble(supportMessage: _msg(author: null)),
SupportChatMessageBubble(supportMessage: _msg(author: 'Robin')),
));

final container = tester.widget<Container>(find.byType(Container));
Expand All @@ -46,28 +57,48 @@ void main() {

final row = tester.widget<Row>(find.byType(Row));
expect(row.mainAxisAlignment, MainAxisAlignment.start);

expect(find.text('Support'), findsOneWidget);
});

testWidgets('AutoResponder message: grey bubble start-aligned, with label',
(tester) async {
await tester.pumpWidget(_host(
SupportChatMessageBubble(supportMessage: _msg(author: 'AutoResponder')),
));

final container = tester.widget<Container>(find.byType(Container));
final decoration = container.decoration! as BoxDecoration;
expect(decoration.color, RealUnitColors.neutral100);

final row = tester.widget<Row>(find.byType(Row));
expect(row.mainAxisAlignment, MainAxisAlignment.start);

expect(find.text('Support'), findsOneWidget);
});

testWidgets('renders the message body', (tester) async {
await tester.pumpWidget(_host(
SupportChatMessageBubble(
supportMessage: _msg(author: 'user-1', body: 'Where is my withdrawal?'),
supportMessage: _msg(author: 'Customer', body: 'Where is my withdrawal?'),
),
));

expect(find.text('Where is my withdrawal?'), findsOneWidget);
});

testWidgets('null body: no message Text rendered (only the timestamp)',
testWidgets(
'null body on support: only label + timestamp remain (no body Text)',
(tester) async {
await tester.pumpWidget(_host(
SupportChatMessageBubble(
supportMessage: _msg(author: null, body: null),
supportMessage: _msg(author: 'Robin', body: null),
),
));

// Only the timestamp Text remains.
expect(find.byType(Text), findsOneWidget);
// Label "Support" + timestamp = 2 Texts.
expect(find.byType(Text), findsNWidgets(2));
expect(find.text('Support'), findsOneWidget);
});
});
}
Loading