Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/Adds content cards widget in Flutter #46

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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 example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class BrazeFunctionsState extends State<BrazeFunctions> {
final customEventPropertyValueController = TextEditingController();
StreamSubscription inAppMessageStreamSubscription;
StreamSubscription contentCardsStreamSubscription;
List<BrazeContentCard> _brazeContentCards = <BrazeContentCard>[];

void initState() {
_braze = new BrazePlugin(customConfigs: {replayCallbacksConfigKey: true});
Expand Down Expand Up @@ -314,6 +315,22 @@ class BrazeFunctionsState extends State<BrazeFunctions> {
));
},
),
Visibility(
visible: _brazeContentCards.isNotEmpty,
child: SectionHeader("ContentCards"),
),
Visibility(
visible: _brazeContentCards.isNotEmpty,
child: Column(
children: _brazeContentCards
.map(
(contentCard) => ContentCard(
contentCard: contentCard,
),
)
.toList(),
),
),
SectionHeader("Other"),
TextButton(
child: const Text('SET LAST KNOWN LOCATION'),
Expand Down Expand Up @@ -467,6 +484,9 @@ class BrazeFunctionsState extends State<BrazeFunctions> {
));
return;
}
setState(() {
_brazeContentCards = contentCards;
});
contentCards.forEach((contentCard) {
print("[$prefix] Received card: " + contentCard.toString());
ScaffoldMessenger.of(context).showSnackBar(new SnackBar(
Expand Down
32 changes: 21 additions & 11 deletions lib/braze_plugin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:convert' as json;
import 'package:flutter/services.dart';

export 'widgets/widgets.dart';

/* Custom configuration keys */
const String replayCallbacksConfigKey = 'ReplayCallbacksKey';

Expand Down Expand Up @@ -598,6 +600,17 @@ enum ClickAction { news_feed, uri, none }
/// Braze in-app message types
enum MessageType { slideup, modal, full, html_full }

/// Braze content card types
enum ContentCardType {
bannerImage('banner_image'),
shortNews('short_news'),
captionedImage('captioned_image');

const ContentCardType(this._jsonValue);

final String _jsonValue;
}

class BrazeContentCard {
/// Content Card json
String contentCardJsonString = "";
Expand Down Expand Up @@ -642,7 +655,7 @@ class BrazeContentCard {
String title = "";

/// Content Card type
String type = "";
ContentCardType type = ContentCardType.shortNews;

/// Content Card url
String url = "";
Expand All @@ -653,9 +666,6 @@ class BrazeContentCard {
/// Content Card viewed
bool viewed = false;

/// Content Card control
bool isControl = false;

BrazeContentCard(String _data) {
contentCardJsonString = _data;
var contentCardJson = json.jsonDecode(_data);
Expand Down Expand Up @@ -718,10 +728,12 @@ class BrazeContentCard {
}
var typeJson = contentCardJson["tp"];
if (typeJson is String) {
type = typeJson;
}
if (type == "control") {
isControl = true;
for (final contentType in ContentCardType.values) {
if (contentType._jsonValue == typeJson) {
type = contentType;
break;
}
}
}
var urlJson = contentCardJson["u"];
if (urlJson is String) {
Expand All @@ -744,7 +756,7 @@ class BrazeContentCard {
" url:" +
url +
" type:" +
type +
type.toString() +
" useWebView:" +
useWebView.toString() +
" title:" +
Expand Down Expand Up @@ -775,8 +787,6 @@ class BrazeContentCard {
expiresAt.toString() +
" dismissable:" +
dismissable.toString() +
" isControl:" +
isControl.toString() +
" contentCardJsonString:" +
contentCardJsonString;
}
Expand Down
208 changes: 208 additions & 0 deletions lib/widgets/content_card.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import 'package:braze_plugin/braze_plugin.dart';
import 'package:flutter/material.dart';

class ContentCard extends StatelessWidget {
const ContentCard({
super.key,
required this.contentCard,
this.cardPressedUrl,
});

final BrazeContentCard contentCard;
final ValueChanged<String>? cardPressedUrl;

@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: contentCard.url.isNotEmpty && cardPressedUrl != null
? () => cardPressedUrl!(contentCard.url)
: null,
child: Column(
children: [
Stack(
children: [
Builder(
builder: (context) {
switch (contentCard.type) {
case ContentCardType.bannerImage:
return BannerImage(
contentCard: contentCard,
);
case ContentCardType.shortNews:
return ShortNews(
contentCard: contentCard,
);
case ContentCardType.captionedImage:
return CaptionedImage(
contentCard: contentCard,
);
}
},
),
Visibility(
visible: contentCard.pinned,
child: const Align(
alignment: Alignment.topRight,
child: StarTriangleBackground(),
),
),
],
),
Visibility(
visible: !contentCard.viewed,
child: const NotViewedCard(),
),
],
),
),
);
}
}

@visibleForTesting
class NotViewedCard extends StatelessWidget {
const NotViewedCard({super.key});

@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Colors.blue,
),
height: 3,
width: double.infinity,
);
}
}

@visibleForTesting
class StarTriangleBackground extends StatelessWidget {
const StarTriangleBackground();

@override
Widget build(BuildContext context) {
return Container(
height: 27,
width: 27,
padding: const EdgeInsets.all(1),
decoration: const BoxDecoration(
gradient: LinearGradient(
stops: [.5, .5],
begin: Alignment.topRight,
end: Alignment.bottomLeft,
colors: [
Colors.blue,
Colors.transparent, // top Right part
],
),
),
child: const Align(
alignment: Alignment.topRight,
child: Icon(
Icons.star,
color: Colors.white,
size: 13,
),
),
);
}
}

@visibleForTesting
class CaptionedImage extends StatelessWidget {
const CaptionedImage({
super.key,
required this.contentCard,
});

final BrazeContentCard contentCard;

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
contentCard.image,
width: double.infinity,
fit: BoxFit.cover,
),
const SizedBox(
height: 16,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
contentCard.title,
style: Theme.of(context).textTheme.headlineSmall,
),
),
const SizedBox(
height: 12,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
contentCard.description,
style: Theme.of(context).textTheme.bodyLarge,
),
),
const SizedBox(
height: 16,
),
],
);
}
}

@visibleForTesting
class ShortNews extends StatelessWidget {
const ShortNews({
super.key,
required this.contentCard,
});

final BrazeContentCard contentCard;

@override
Widget build(BuildContext context) {
return ListTile(
leading: contentCard.image.isNotEmpty
? Image.network(
contentCard.image,
width: 60,
)
: null,
contentPadding: const EdgeInsets.all(16),
title: Text(
contentCard.title,
style: Theme.of(context).textTheme.headlineSmall,
),
subtitle: Text(
contentCard.description,
style: Theme.of(context).textTheme.bodyLarge,
),
);
}
}

@visibleForTesting
class BannerImage extends StatelessWidget {
const BannerImage({
super.key,
required this.contentCard,
});

final BrazeContentCard contentCard;

@override
Widget build(BuildContext context) {
return Center(
child: Image.network(
contentCard.image,
),
);
}
}
1 change: 1 addition & 0 deletions lib/widgets/widgets.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'content_card.dart';
4 changes: 3 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ homepage: https://www.braze.com/
repository: https://github.com/braze-inc/braze-flutter-sdk

environment:
sdk: ">=2.12.0 <3.0.0"
sdk: ">=2.17.0 <3.0.0"
flutter: ">=1.10.0"

dependencies:
Expand All @@ -15,6 +15,8 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
mocktail: ^0.3.0
mocktail_image_network: ^0.3.1

flutter:
plugin:
Expand Down
23 changes: 17 additions & 6 deletions test/braze_plugin_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,22 @@ void main() {
]);
});

test('should include isControl field', () {
BrazePlugin _braze = new BrazePlugin();
String _data = '{"tp":"control"}';
test('should parse ContentCardType to a bannerImage', () {
String _data = '{"tp":"banner_image"}';
BrazeContentCard _contentCard = new BrazeContentCard(_data);
expect(_contentCard.type, equals(ContentCardType.bannerImage));
});

test('should parse ContentCardType to a shortNews', () {
String _data = '{"tp":"short_news"}';
BrazeContentCard _contentCard = new BrazeContentCard(_data);
expect(_contentCard.type, equals(ContentCardType.shortNews));
});

test('should parse ContentCardType to a captionedImage', () {
String _data = '{"tp":"captioned_image"}';
BrazeContentCard _contentCard = new BrazeContentCard(_data);
expect(_contentCard.isControl, equals(true));
expect(_contentCard.type, equals(ContentCardType.captionedImage));
});

test('should call logContentCardImpression', () {
Expand Down Expand Up @@ -1060,7 +1071,7 @@ void main() {
bool testPinned = true;
bool testRemoved = false;
String testTitle = "some title";
String testType = "some type";
String testType = "banner_image";
String testUri = "https:\\/\\/www.sometesturi.com";
bool testUseWebView = true;
bool testViewed = false;
Expand All @@ -1084,7 +1095,7 @@ void main() {
expect(contentCard.pinned, equals(testPinned));
expect(contentCard.removed, equals(testRemoved));
expect(contentCard.title, equals(testTitle));
expect(contentCard.type, equals(testType));
expect(contentCard.type, equals(ContentCardType.bannerImage));
expect(contentCard.url, equals(json.jsonDecode('"$testUri"')));
expect(contentCard.useWebView, equals(testUseWebView));
expect(contentCard.viewed, equals(testViewed));
Expand Down
Loading