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

Add share feature #2728

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
109 changes: 105 additions & 4 deletions components/CollapsedQR.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import {
TouchableOpacity,
TouchableWithoutFeedback
} from 'react-native';
import QRCode from 'react-native-qrcode-svg';

import QRCode, { QRCodeProps } from 'react-native-qrcode-svg';
import HCESession, { NFCContentType, NFCTagType4 } from 'react-native-hce';

import Amount from './Amount';
import Button from './Button';
import CopyButton from './CopyButton';
import ShareButton from './ShareButton';
import { localeString } from './../utils/LocaleUtils';
import { themeColor } from './../utils/ThemeUtils';
import Touchable from './Touchable';
Expand All @@ -26,11 +27,36 @@ const defaultLogoWhite = require('../assets/images/icon-white.png');

let simulation: any;

type QRCodeElement = React.ElementRef<typeof QRCode>;

interface ExtendedQRCodeProps
extends QRCodeProps,
React.RefAttributes<QRCodeElement> {
onLoad?: () => void;
parent?: CollapsedQR;
}

interface ValueTextProps {
value: string;
truncateLongValue?: boolean;
}

// Custom QR code component that forwards refs and handles component readiness
// Sets qrReady state on first valid component mount to prevent remounting cycles
const ForwardedQRCode = React.forwardRef<QRCodeElement, ExtendedQRCodeProps>(
(props, ref) => (
<QRCode
{...props}
getRef={(c) => {
if (c && c.toDataURL && !(ref as any).current) {
(ref as any).current = c;
props.parent?.setState({ qrReady: true });
}
}}
/>
)
) as React.FC<ExtendedQRCodeProps>;

function ValueText({ value, truncateLongValue }: ValueTextProps) {
const [state, setState] = React.useState<{
numberOfValueLines: number | undefined;
Expand Down Expand Up @@ -64,6 +90,9 @@ interface CollapsedQRProps {
collapseText?: string;
copyText?: string;
copyValue?: string;
copyIconContainerStyle?: any;
showShare?: boolean;
iconOnly?: boolean;
hideText?: boolean;
expanded?: boolean;
textBottom?: boolean;
Expand All @@ -78,16 +107,22 @@ interface CollapsedQRState {
collapsed: boolean;
nfcBroadcast: boolean;
enlargeQR: boolean;
tempQRRef: React.RefObject<QRCodeElement> | null;
qrReady: boolean;
}

export default class CollapsedQR extends React.Component<
CollapsedQRProps,
CollapsedQRState
> {
qrRef = React.createRef<QRCodeElement>();

state = {
collapsed: this.props.expanded ? false : true,
nfcBroadcast: false,
enlargeQR: false
enlargeQR: false,
tempQRRef: null,
qrReady: false
};

componentWillUnmount() {
Expand Down Expand Up @@ -134,13 +169,16 @@ export default class CollapsedQR extends React.Component<
};

render() {
const { collapsed, nfcBroadcast, enlargeQR } = this.state;
const { collapsed, nfcBroadcast, enlargeQR, tempQRRef } = this.state;
const {
value,
showText,
copyText,
copyValue,
collapseText,
copyIconContainerStyle,
showShare,
iconOnly,
hideText,
expanded,
textBottom,
Expand All @@ -151,8 +189,42 @@ export default class CollapsedQR extends React.Component<

const { width, height } = Dimensions.get('window');

// Creates a temporary QR code for sharing and waits for component to be ready
// Returns a promise that resolves when QR is fully rendered and ready to be captured
const handleShare = () =>
new Promise<void>((resolve) => {
const tempRef = React.createRef<QRCodeElement>();
this.setState({ tempQRRef: tempRef, qrReady: false }, () => {
const checkReady = () => {
if (this.state.qrReady) {
resolve();
} else {
requestAnimationFrame(checkReady);
}
};
checkReady();
});
});

return (
<React.Fragment>
{/* Temporary QR for sharing */}
{tempQRRef && (
<View style={{ height: 0, width: 0, overflow: 'hidden' }}>
<ForwardedQRCode
ref={tempQRRef}
value={value}
size={800}
logo={defaultLogo}
backgroundColor={'white'}
logoBackgroundColor={'white'}
logoMargin={10}
quietZone={40}
parent={this}
/>
</View>
)}

{satAmount != null && this.props.displayAmount && (
<View
style={{
Expand Down Expand Up @@ -277,7 +349,36 @@ export default class CollapsedQR extends React.Component<
onPress={() => this.toggleCollapse()}
/>
)}
<CopyButton copyValue={copyValue || value} title={copyText} />
{showShare ? (
<View
style={{
flexDirection: 'row',
justifyContent: 'center'
}}
>
<CopyButton
copyIconContainerStyle={copyIconContainerStyle}
copyValue={copyValue || value}
title={copyText}
iconOnly={iconOnly}
/>
<ShareButton
value={copyValue || value}
qrRef={tempQRRef}
iconOnly={iconOnly}
onPress={handleShare}
onShareComplete={() =>
this.setState({ tempQRRef: null })
}
/>
</View>
) : (
<CopyButton
copyValue={copyValue || value}
title={copyText}
iconOnly={iconOnly}
/>
)}
{Platform.OS === 'android' && this.props.nfcSupported && (
<Button
title={
Expand Down
99 changes: 99 additions & 0 deletions components/ShareButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as React from 'react';
import { Platform, TouchableOpacity } from 'react-native';

import QRCode from 'react-native-qrcode-svg';
import { captureRef } from 'react-native-view-shot';
import Share from 'react-native-share';
import { Icon } from 'react-native-elements';

import Button from './../components/Button';

import { localeString } from './../utils/LocaleUtils';
import { themeColor } from './../utils/ThemeUtils';

type QRCodeElement = React.ElementRef<typeof QRCode>;

interface ShareButtonProps {
value: string;
qrRef: React.RefObject<QRCodeElement> | null;
title?: string;
icon?: any;
noUppercase?: boolean;
iconOnly?: boolean;
onPress: () => Promise<void>;
onShareComplete?: () => void;
}

export default class ShareButton extends React.Component<ShareButtonProps> {
handlePress = async () => {
const { onPress } = this.props;
await onPress();
await this.shareContent();
};

shareContent = async () => {
const { value, qrRef, onShareComplete } = this.props;
try {
if (!qrRef?.current) {
return;
}

const svgElement = qrRef.current;
const base64Data = await captureRef(svgElement, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@myxmaster It looks like the QR logo issue on iOS happens because the capture runs before the logo fully loads. Try adding a 500ms delay here — should fix it!
await new Promise((resolve) => setTimeout(resolve, 500));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, thanks for looking into it! I will try to fix it, if possible without a static timeout.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shubhamkmr04 Can you please test if latest commit fixes the issue?

Copy link
Contributor

@shubhamkmr04 shubhamkmr04 Feb 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we still have that issue in iOS

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm ok. Any other idea?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, let's see if @kaloudis has an idea.
An additional 500ms delay is not exactly nice for performance...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, let's see if @kaloudis has an idea.
An additional 500ms delay is not exactly nice for performance...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup I agree. 50ms delay is also working though if we dont find any other solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that would be an acceptable workaround, but still not really cool :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not ideal. Let's see if we can figure out an alternative approach.

format: 'png',
quality: 1
});

await Share.open({
message: value,
url: base64Data
});
} catch (error) {
// Share API throws error when share sheet closes, regardless of success
console.log('Error in shareContent:', error);
} finally {
onShareComplete?.();
}
};

render() {
const { title, icon, noUppercase, iconOnly } = this.props;

if (iconOnly) {
return (
<TouchableOpacity
// "padding: 5" leads to a larger area where users can click on
style={{ padding: 5 }}
onPress={this.handlePress}
>
<Icon
name={'share'}
size={27}
color={themeColor('secondaryText')}
/>
</TouchableOpacity>
);
}

return (
<Button
title={title || localeString('general.share')}
icon={
icon
? icon
: {
name: 'share',
size: 25
}
}
containerStyle={{
marginTop: 10,
marginBottom: Platform.OS === 'android' ? 0 : 20
}}
onPress={this.handlePress}
secondary
noUppercase={noUppercase}
/>
);
}
}
1 change: 1 addition & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"general.close": "Close",
"general.hide": "Hide",
"general.copy": "Copy",
"general.share": "Share",
"general.goBack": "Go Back",
"general.lightning": "Lightning",
"general.onchain": "On-chain",
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"react-native-safe-area-context": "4.10.1",
"react-native-screens": "3.31.1",
"react-native-securerandom": "1.0.1",
"react-native-share": "12.0.3",
"react-native-svg": "15.3.0",
"react-native-svg-transformer": "1.3.0",
"react-native-system-navigation-bar": "2.6.4",
Expand All @@ -128,6 +129,7 @@
"react-native-udp": "4.1.7",
"react-native-v8": "0.61.5-patch.4",
"react-native-vector-icons": "7.1.0",
"react-native-view-shot": "4.0.3",
"react-native-vision-camera": "4.3.2",
"readable-stream": "1.0.33",
"sha.js": "2.4.11",
Expand Down
40 changes: 25 additions & 15 deletions views/Receive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1851,9 +1851,11 @@ export default class Receive extends React.Component<
haveUnifiedInvoice && (
<CollapsedQR
value={unifiedInvoice || ''}
copyText={localeString(
'views.Receive.copyInvoice'
)}
iconOnly={true}
copyIconContainerStyle={{
marginRight: 40
}}
showShare={true}
expanded
textBottom
truncateLongValue
Expand Down Expand Up @@ -1881,9 +1883,11 @@ export default class Receive extends React.Component<
copyValue={
lnInvoiceCopyValue
}
copyText={localeString(
'views.Receive.copyInvoice'
)}
iconOnly={true}
copyIconContainerStyle={{
marginRight: 40
}}
showShare={true}
expanded
textBottom
truncateLongValue
Expand Down Expand Up @@ -1911,9 +1915,11 @@ export default class Receive extends React.Component<
copyValue={
btcAddressCopyValue
}
copyText={localeString(
'views.Receive.copyAddress'
)}
iconOnly={true}
copyIconContainerStyle={{
marginRight: 40
}}
showShare={true}
expanded
textBottom
truncateLongValue
Expand Down Expand Up @@ -1990,9 +1996,11 @@ export default class Receive extends React.Component<
lightningAddress && (
<CollapsedQR
value={`lightning:${lightningAddress}`}
copyText={localeString(
'views.Receive.copyAddress'
)}
iconOnly={true}
copyIconContainerStyle={{
marginRight: 40
}}
showShare={true}
expanded
textBottom
hideText
Expand Down Expand Up @@ -2023,9 +2031,11 @@ export default class Receive extends React.Component<
copyValue={
lnInvoiceCopyValue
}
copyText={localeString(
'views.Receive.copyInvoice'
)}
iconOnly={true}
copyIconContainerStyle={{
marginRight: 40
}}
showShare={true}
expanded
textBottom
truncateLongValue
Expand Down
Loading
Loading