Merge branch 'main' of https://github.com/cake-tech/cake_wallet into CW-519-tor

This commit is contained in:
Matthew Fosse 2024-03-04 07:52:39 -08:00
commit 4e8fd46990
9 changed files with 200 additions and 86 deletions

View file

@ -270,11 +270,11 @@ class AddressValidator extends TextValidator {
'|([^0-9a-zA-Z]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)';
case CryptoCurrency.btc:
return '([^0-9a-zA-Z]|^)${P2pkhAddress.regex.pattern}|\$)'
'([^0-9a-zA-Z]|^)${P2shAddress.regex.pattern}|\$)'
'([^0-9a-zA-Z]|^)${P2wpkhAddress.regex.pattern}|\$)'
'([^0-9a-zA-Z]|^)${P2wshAddress.regex.pattern}|\$)'
'([^0-9a-zA-Z]|^)${P2trAddress.regex.pattern}|\$)';
return '([^0-9a-zA-Z]|^)([1mn][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2pkhAddress type
'|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)'; //P2trAddress type
case CryptoCurrency.ltc:
return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)'

View file

@ -69,16 +69,20 @@ class AddressResolver {
}
Future<ParsedAddress> resolve(BuildContext context, String text, String ticker) async {
Future<ParsedAddress> resolve(BuildContext context, String text, String ticker) async {
try {
if (text.startsWith('@') && !text.substring(1).contains('@')) {
if(settingsStore.lookupsTwitter) {
if (settingsStore.lookupsTwitter) {
final formattedName = text.substring(1);
final twitterUser = await TwitterApi.lookupUserByName(userName: formattedName);
final addressFromBio = extractAddressByType(
raw: twitterUser.description, type: CryptoCurrency.fromString(ticker));
if (addressFromBio != null) {
return ParsedAddress.fetchTwitterAddress(address: addressFromBio, name: text);
return ParsedAddress.fetchTwitterAddress(
address: addressFromBio,
name: text,
profileImageUrl: twitterUser.profileImageUrl,
profileName: twitterUser.name);
}
final pinnedTweet = twitterUser.pinnedTweet?.text;
@ -86,7 +90,11 @@ class AddressResolver {
final addressFromPinnedTweet =
extractAddressByType(raw: pinnedTweet, type: CryptoCurrency.fromString(ticker));
if (addressFromPinnedTweet != null) {
return ParsedAddress.fetchTwitterAddress(address: addressFromPinnedTweet, name: text);
return ParsedAddress.fetchTwitterAddress(
address: addressFromPinnedTweet,
name: text,
profileImageUrl: twitterUser.profileImageUrl,
profileName: twitterUser.name);
}
}
}
@ -107,7 +115,11 @@ class AddressResolver {
extractAddressByType(raw: mastodonUser.note, type: CryptoCurrency.fromString(ticker));
if (addressFromBio != null) {
return ParsedAddress.fetchMastodonAddress(address: addressFromBio, name: text);
return ParsedAddress.fetchMastodonAddress(
address: addressFromBio,
name: text,
profileImageUrl: mastodonUser.profileImageUrl,
profileName: mastodonUser.username);
} else {
final pinnedPosts =
await MastodonAPI.getPinnedPosts(userId: mastodonUser.id, apiHost: hostName);
@ -119,7 +131,10 @@ class AddressResolver {
if (addressFromPinnedPost != null) {
return ParsedAddress.fetchMastodonAddress(
address: addressFromPinnedPost, name: text);
address: addressFromPinnedPost,
name: text,
profileImageUrl: mastodonUser.profileImageUrl,
profileName: mastodonUser.username);
}
}
}

View file

@ -1,7 +1,6 @@
import 'package:cake_wallet/entities/openalias_record.dart';
import 'package:cake_wallet/entities/yat_record.dart';
enum ParseFrom {
unstoppableDomains,
openAlias,
@ -20,36 +19,37 @@ class ParsedAddress {
required this.addresses,
this.name = '',
this.description = '',
this.profileImageUrl = '',
this.profileName = '',
this.parseFrom = ParseFrom.notParsed,
});
factory ParsedAddress.fetchEmojiAddress({
List<YatRecord>? addresses,
required String name,
}){
if (addresses?.isEmpty ?? true) {
return ParsedAddress(
addresses: [name], parseFrom: ParseFrom.yatRecord);
}
return ParsedAddress(
addresses: addresses!.map((e) => e.address).toList(),
name: name,
parseFrom: ParseFrom.yatRecord,
);
}) {
if (addresses?.isEmpty ?? true) {
return ParsedAddress(addresses: [name], parseFrom: ParseFrom.yatRecord);
}
return ParsedAddress(
addresses: addresses!.map((e) => e.address).toList(),
name: name,
parseFrom: ParseFrom.yatRecord,
);
}
factory ParsedAddress.fetchUnstoppableDomainAddress({
String? address,
required String name,
}){
if (address?.isEmpty ?? true) {
return ParsedAddress(addresses: [name]);
}
return ParsedAddress(
addresses: [address!],
name: name,
parseFrom: ParseFrom.unstoppableDomains,
);
}) {
if (address?.isEmpty ?? true) {
return ParsedAddress(addresses: [name]);
}
return ParsedAddress(
addresses: [address!],
name: name,
parseFrom: ParseFrom.unstoppableDomains,
);
}
factory ParsedAddress.fetchOpenAliasAddress(
@ -65,7 +65,7 @@ class ParsedAddress {
);
}
factory ParsedAddress.fetchFioAddress({required String address, required String name}){
factory ParsedAddress.fetchFioAddress({required String address, required String name}) {
return ParsedAddress(
addresses: [address],
name: name,
@ -73,23 +73,37 @@ class ParsedAddress {
);
}
factory ParsedAddress.fetchTwitterAddress({required String address, required String name}){
factory ParsedAddress.fetchTwitterAddress(
{required String address,
required String name,
required String profileImageUrl,
required String profileName,
String? description}) {
return ParsedAddress(
addresses: [address],
name: name,
description: description ?? '',
profileImageUrl: profileImageUrl,
profileName: profileName,
parseFrom: ParseFrom.twitter,
);
}
factory ParsedAddress.fetchMastodonAddress({required String address, required String name}){
factory ParsedAddress.fetchMastodonAddress(
{required String address,
required String name,
required String profileImageUrl,
required String profileName}) {
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.mastodon
parseFrom: ParseFrom.mastodon,
profileImageUrl: profileImageUrl,
profileName: profileName,
);
}
factory ParsedAddress.fetchContactAddress({required String address, required String name}){
factory ParsedAddress.fetchContactAddress({required String address, required String name}) {
return ParsedAddress(
addresses: [address],
name: name,
@ -116,6 +130,7 @@ class ParsedAddress {
final List<String> addresses;
final String name;
final String description;
final String profileImageUrl;
final String profileName;
final ParseFrom parseFrom;
}

View file

@ -1,12 +1,14 @@
class MastodonUser {
String id;
String username;
String profileImageUrl;
String acct;
String note;
MastodonUser({
required this.id,
required this.username,
required this.profileImageUrl,
required this.acct,
required this.note,
});
@ -14,9 +16,10 @@ class MastodonUser {
factory MastodonUser.fromJson(Map<String, dynamic> json) {
return MastodonUser(
id: json['id'] as String,
username: json['username'] as String,
username: json['username'] as String? ?? '',
acct: json['acct'] as String,
note: json['note'] as String,
profileImageUrl: json['avatar'] as String? ?? ''
);
}
}

View file

@ -11,6 +11,8 @@ Future<String> extractAddressFromParsed(
var title = '';
var content = '';
var address = '';
var profileImageUrl = '';
var profileName = '';
switch (parsedAddress.parseFrom) {
case ParseFrom.unstoppableDomains:
@ -37,11 +39,15 @@ Future<String> extractAddressFromParsed(
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (Twitter)');
address = parsedAddress.addresses.first;
profileImageUrl = parsedAddress.profileImageUrl;
profileName = parsedAddress.profileName;
break;
case ParseFrom.mastodon:
title = S.of(context).address_detected;
content = S.of(context).extracted_address_content('${parsedAddress.name} (Mastodon)');
address = parsedAddress.addresses.first;
profileImageUrl = parsedAddress.profileImageUrl;
profileName = parsedAddress.profileName;
break;
case ParseFrom.nostr:
title = S.of(context).address_detected;
@ -95,6 +101,8 @@ Future<String> extractAddressFromParsed(
return AlertWithOneAction(
alertTitle: title,
headerTitleText: profileName.isEmpty ? null : profileName,
headerImageProfileUrl: profileImageUrl.isEmpty ? null : profileImageUrl,
alertContent: content,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());

View file

@ -7,7 +7,9 @@ class AlertWithOneAction extends BaseAlertDialog {
required this.alertContent,
required this.buttonText,
required this.buttonAction,
this.alertBarrierDismissible = true
this.alertBarrierDismissible = true,
this.headerTitleText,
this.headerImageProfileUrl
});
final String alertTitle;
@ -15,6 +17,8 @@ class AlertWithOneAction extends BaseAlertDialog {
final String buttonText;
final VoidCallback buttonAction;
final bool alertBarrierDismissible;
final String? headerTitleText;
final String? headerImageProfileUrl;
@override
String get titleText => alertTitle;
@ -25,6 +29,12 @@ class AlertWithOneAction extends BaseAlertDialog {
@override
bool get barrierDismissible => alertBarrierDismissible;
@override
String? get headerImageUrl => headerImageProfileUrl;
@override
String? get headerText => headerTitleText;
@override
Widget actionButtons(BuildContext context) {
return Container(

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/src/screens/receive/widgets/header_tile.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'dart:ui';
import 'package:cake_wallet/src/widgets/section_divider.dart';
@ -5,19 +6,34 @@ import 'package:cake_wallet/themes/extensions/alert_theme.dart';
import 'package:flutter/material.dart';
class BaseAlertDialog extends StatelessWidget {
String? get headerText => '';
String get titleText => '';
String get contentText => '';
String get leftActionButtonText => '';
String get rightActionButtonText => '';
bool get isDividerExists => false;
VoidCallback get actionLeft => () {};
VoidCallback get actionRight => () {};
bool get barrierDismissible => true;
Color? get leftActionButtonTextColor => null;
Color? get rightActionButtonTextColor => null;
Color? get leftActionButtonColor => null;
Color? get rightActionButtonColor => null;
String? get headerImageUrl => null;
Widget title(BuildContext context) {
return Text(
titleText,
@ -32,6 +48,23 @@ class BaseAlertDialog extends StatelessWidget {
);
}
Widget headerTitle(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 10.0),
child: Text(
headerText!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none,
),
),
);
}
Widget content(BuildContext context) {
return Text(
contentText,
@ -48,17 +81,17 @@ class BaseAlertDialog extends StatelessWidget {
Widget actionButtons(BuildContext context) {
return Container(
height: 60,
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
height: 60,
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: TextButton(
onPressed: actionLeft,
style: TextButton.styleFrom(
backgroundColor: leftActionButtonColor ??
Theme.of(context).dialogBackgroundColor,
backgroundColor:
leftActionButtonColor ?? Theme.of(context).dialogBackgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.zero))),
child: Text(
@ -79,8 +112,8 @@ class BaseAlertDialog extends StatelessWidget {
child: TextButton(
onPressed: actionRight,
style: TextButton.styleFrom(
backgroundColor: rightActionButtonColor ??
Theme.of(context).dialogBackgroundColor,
backgroundColor:
rightActionButtonColor ?? Theme.of(context).dialogBackgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.zero))),
child: Text(
@ -90,8 +123,7 @@ class BaseAlertDialog extends StatelessWidget {
fontSize: 15,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: rightActionButtonTextColor ??
Theme.of(context).primaryColor,
color: rightActionButtonTextColor ?? Theme.of(context).primaryColor,
decoration: TextDecoration.none,
),
)),
@ -100,6 +132,24 @@ class BaseAlertDialog extends StatelessWidget {
));
}
Widget headerImage(BuildContext context, String imageUrl) {
return Positioned(
top: -50,
left: 0,
right: 0,
child: CircleAvatar(
radius: 50,
backgroundColor: Colors.white,
child: ClipOval(
child: Image.network(
imageUrl,
fit: BoxFit.cover,
),
),
),
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
@ -109,43 +159,51 @@ class BaseAlertDialog extends StatelessWidget {
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0),
child: Container(
decoration: BoxDecoration(
color:
Theme.of(context).extension<AlertTheme>()!.backdropColor),
decoration:
BoxDecoration(color: Theme.of(context).extension<AlertTheme>()!.backdropColor),
child: Center(
child: GestureDetector(
onTap: () => null,
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(30)),
child: Container(
width: 300,
color: Theme.of(context).dialogBackgroundColor,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(24, 20, 24, 0),
child: title(context),
),
isDividerExists
? Padding(
padding: EdgeInsets.only(top: 16, bottom: 8),
child: const HorizontalSectionDivider(),
)
: Offstage(),
Padding(
padding: EdgeInsets.fromLTRB(24, 8, 24, 32),
child: content(context),
)
],
),
const HorizontalSectionDivider(),
actionButtons(context)
],
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Theme.of(context).dialogBackgroundColor),
width: 300,
child: Stack(
clipBehavior: Clip.none,
children: [
if (headerImageUrl != null) headerImage(context, headerImageUrl!),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (headerImageUrl != null) const SizedBox(height: 50),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
if (headerText != null) headerTitle(context),
Padding(
padding: EdgeInsets.fromLTRB(24, 20, 24, 0),
child: title(context),
),
isDividerExists
? Padding(
padding: EdgeInsets.only(top: 16, bottom: 8),
child: const HorizontalSectionDivider(),
)
: Offstage(),
Padding(
padding: EdgeInsets.fromLTRB(24, 8, 24, 32),
child: content(context),
)
],
),
const HorizontalSectionDivider(),
ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(30)),
child: actionButtons(context))
],
),
],
),
),
),

View file

@ -12,7 +12,7 @@ class TwitterApi {
static Future<TwitterUser> lookupUserByName({required String userName}) async {
final queryParams = {
'user.fields': 'description',
'user.fields': 'description,profile_image_url',
'expansions': 'pinned_tweet_id',
'tweet.fields': 'note_tweet'
};

View file

@ -4,20 +4,25 @@ class TwitterUser {
required this.username,
required this.name,
required this.description,
required this.profileImageUrl,
this.pinnedTweet});
final String id;
final String username;
final String name;
final String description;
final String profileImageUrl;
final Tweet? pinnedTweet;
factory TwitterUser.fromJson(Map<String, dynamic> json, [Tweet? pinnedTweet]) {
final profileImageUrl = json['data']['profile_image_url'] as String? ?? '';
final scaledProfileImageUrl = profileImageUrl.replaceFirst('normal', '200x200');
return TwitterUser(
id: json['data']['id'] as String,
username: json['data']['username'] as String,
username: json['data']['username'] as String? ?? '',
name: json['data']['name'] as String,
description: json['data']['description'] as String? ?? '',
profileImageUrl: scaledProfileImageUrl,
pinnedTweet: pinnedTweet,
);
}