mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-22 19:49:22 +00:00
Merge remote-tracking branch 'origin/main' into CW-453-silent-payments
This commit is contained in:
commit
9faba04dc2
9 changed files with 200 additions and 86 deletions
|
@ -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
|
||||
'|${SilentPaymentAddress.regex.pattern}\$';
|
||||
case CryptoCurrency.ltc:
|
||||
return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
|
||||
|
|
|
@ -72,13 +72,17 @@ class AddressResolver {
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,16 +19,17 @@ 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: [name], parseFrom: ParseFrom.yatRecord);
|
||||
}
|
||||
return ParsedAddress(
|
||||
addresses: addresses!.map((e) => e.address).toList(),
|
||||
|
@ -41,7 +41,7 @@ class ParsedAddress {
|
|||
factory ParsedAddress.fetchUnstoppableDomainAddress({
|
||||
String? address,
|
||||
required String name,
|
||||
}){
|
||||
}) {
|
||||
if (address?.isEmpty ?? true) {
|
||||
return ParsedAddress(addresses: [name]);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -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? ?? ''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
@ -57,8 +90,8 @@ class BaseAlertDialog extends StatelessWidget {
|
|||
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,23 +159,28 @@ 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(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
color: Theme.of(context).dialogBackgroundColor),
|
||||
width: 300,
|
||||
color: Theme.of(context).dialogBackgroundColor,
|
||||
child: Column(
|
||||
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),
|
||||
|
@ -143,9 +198,12 @@ class BaseAlertDialog extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
const HorizontalSectionDivider(),
|
||||
actionButtons(context)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(30)),
|
||||
child: actionButtons(context))
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue