@dubsdotapp/expo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,173 @@
1
+ import React from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ ScrollView,
6
+ TouchableOpacity,
7
+ ActivityIndicator,
8
+ StyleSheet,
9
+ } from 'react-native';
10
+ import { useDubsTheme } from './theme';
11
+ import { UserProfileCard } from './UserProfileCard';
12
+
13
+ export interface SettingsSheetProps {
14
+ walletAddress: string;
15
+ username?: string;
16
+ avatarUrl?: string | null;
17
+ memberSince?: string | null;
18
+ appVersion?: string;
19
+ onCopyAddress?: () => void;
20
+ onSupport?: () => void;
21
+ onLogout: () => void | Promise<void>;
22
+ loggingOut?: boolean;
23
+ }
24
+
25
+ function truncateAddress(address: string, chars = 4): string {
26
+ if (address.length <= chars * 2 + 3) return address;
27
+ return `${address.slice(0, chars)}...${address.slice(-chars)}`;
28
+ }
29
+
30
+ export function SettingsSheet({
31
+ walletAddress,
32
+ username,
33
+ avatarUrl,
34
+ memberSince,
35
+ appVersion,
36
+ onCopyAddress,
37
+ onSupport,
38
+ onLogout,
39
+ loggingOut = false,
40
+ }: SettingsSheetProps) {
41
+ const t = useDubsTheme();
42
+
43
+ return (
44
+ <ScrollView
45
+ style={[styles.container, { backgroundColor: t.background }]}
46
+ contentContainerStyle={styles.content}
47
+ >
48
+ {/* Profile card */}
49
+ <UserProfileCard
50
+ walletAddress={walletAddress}
51
+ username={username}
52
+ avatarUrl={avatarUrl}
53
+ memberSince={memberSince}
54
+ />
55
+
56
+ {/* Action rows */}
57
+ <View style={[styles.actionsCard, { backgroundColor: t.surface, borderColor: t.border }]}>
58
+ {onCopyAddress ? (
59
+ <TouchableOpacity
60
+ style={styles.actionRow}
61
+ onPress={onCopyAddress}
62
+ activeOpacity={0.7}
63
+ >
64
+ <View style={styles.actionRowLeft}>
65
+ <Text style={[styles.actionLabel, { color: t.text }]}>Wallet Address</Text>
66
+ <Text style={[styles.actionValue, { color: t.textMuted }]}>
67
+ {truncateAddress(walletAddress)}
68
+ </Text>
69
+ </View>
70
+ <Text style={[styles.copyLabel, { color: t.accent }]}>Copy</Text>
71
+ </TouchableOpacity>
72
+ ) : null}
73
+
74
+ {onSupport ? (
75
+ <>
76
+ {onCopyAddress ? (
77
+ <View style={[styles.separator, { backgroundColor: t.border }]} />
78
+ ) : null}
79
+ <TouchableOpacity
80
+ style={styles.actionRow}
81
+ onPress={onSupport}
82
+ activeOpacity={0.7}
83
+ >
84
+ <Text style={[styles.actionLabel, { color: t.text }]}>Help & Support</Text>
85
+ <Text style={[styles.chevron, { color: t.textMuted }]}>{'\u203A'}</Text>
86
+ </TouchableOpacity>
87
+ </>
88
+ ) : null}
89
+ </View>
90
+
91
+ {/* Logout button */}
92
+ <TouchableOpacity
93
+ style={[styles.logoutButton, { borderColor: t.live }]}
94
+ onPress={onLogout}
95
+ disabled={loggingOut}
96
+ activeOpacity={0.7}
97
+ >
98
+ {loggingOut ? (
99
+ <ActivityIndicator color={t.live} size="small" />
100
+ ) : (
101
+ <Text style={[styles.logoutText, { color: t.live }]}>Log Out</Text>
102
+ )}
103
+ </TouchableOpacity>
104
+
105
+ {/* App version */}
106
+ {appVersion ? (
107
+ <Text style={[styles.version, { color: t.textDim }]}>v{appVersion}</Text>
108
+ ) : null}
109
+ </ScrollView>
110
+ );
111
+ }
112
+
113
+ const styles = StyleSheet.create({
114
+ container: {
115
+ flex: 1,
116
+ },
117
+ content: {
118
+ padding: 20,
119
+ paddingTop: 24,
120
+ gap: 20,
121
+ },
122
+ actionsCard: {
123
+ borderRadius: 16,
124
+ borderWidth: 1,
125
+ overflow: 'hidden',
126
+ },
127
+ actionRow: {
128
+ flexDirection: 'row',
129
+ alignItems: 'center',
130
+ justifyContent: 'space-between',
131
+ paddingHorizontal: 16,
132
+ paddingVertical: 14,
133
+ },
134
+ actionRowLeft: {
135
+ flex: 1,
136
+ gap: 2,
137
+ },
138
+ actionLabel: {
139
+ fontSize: 15,
140
+ fontWeight: '600',
141
+ },
142
+ actionValue: {
143
+ fontSize: 13,
144
+ fontFamily: 'monospace',
145
+ },
146
+ copyLabel: {
147
+ fontSize: 14,
148
+ fontWeight: '600',
149
+ },
150
+ chevron: {
151
+ fontSize: 22,
152
+ fontWeight: '300',
153
+ },
154
+ separator: {
155
+ height: 1,
156
+ marginHorizontal: 16,
157
+ },
158
+ logoutButton: {
159
+ height: 52,
160
+ borderRadius: 16,
161
+ borderWidth: 1.5,
162
+ justifyContent: 'center',
163
+ alignItems: 'center',
164
+ },
165
+ logoutText: {
166
+ fontSize: 16,
167
+ fontWeight: '700',
168
+ },
169
+ version: {
170
+ fontSize: 12,
171
+ textAlign: 'center',
172
+ },
173
+ });
@@ -0,0 +1,90 @@
1
+ import React, { useMemo } from 'react';
2
+ import { View, Text, Image, StyleSheet } from 'react-native';
3
+ import { useDubsTheme } from './theme';
4
+
5
+ export interface UserProfileCardProps {
6
+ walletAddress: string;
7
+ username?: string;
8
+ avatarUrl?: string | null;
9
+ memberSince?: string | null;
10
+ }
11
+
12
+ function truncateAddress(address: string, chars = 4): string {
13
+ if (address.length <= chars * 2 + 3) return address;
14
+ return `${address.slice(0, chars)}...${address.slice(-chars)}`;
15
+ }
16
+
17
+ function formatMemberSince(iso: string): string {
18
+ const date = new Date(iso);
19
+ const month = date.toLocaleString('en-US', { month: 'short' });
20
+ const year = date.getFullYear();
21
+ return `Member since ${month} ${year}`;
22
+ }
23
+
24
+ export function UserProfileCard({
25
+ walletAddress,
26
+ username,
27
+ avatarUrl,
28
+ memberSince,
29
+ }: UserProfileCardProps) {
30
+ const t = useDubsTheme();
31
+
32
+ const imageUri = useMemo(
33
+ () =>
34
+ avatarUrl ||
35
+ `https://api.dicebear.com/9.x/avataaars/png?seed=${walletAddress}&size=128`,
36
+ [avatarUrl, walletAddress],
37
+ );
38
+
39
+ return (
40
+ <View style={[styles.card, { backgroundColor: t.surface, borderColor: t.border }]}>
41
+ <Image source={{ uri: imageUri }} style={styles.avatar} />
42
+ <View style={styles.info}>
43
+ {username ? (
44
+ <Text style={[styles.username, { color: t.text }]}>{username}</Text>
45
+ ) : null}
46
+ <Text style={[styles.address, { color: t.textMuted }]}>
47
+ {truncateAddress(walletAddress)}
48
+ </Text>
49
+ {memberSince ? (
50
+ <Text style={[styles.memberSince, { color: t.textDim }]}>
51
+ {formatMemberSince(memberSince)}
52
+ </Text>
53
+ ) : null}
54
+ </View>
55
+ </View>
56
+ );
57
+ }
58
+
59
+ const styles = StyleSheet.create({
60
+ card: {
61
+ flexDirection: 'row',
62
+ alignItems: 'center',
63
+ padding: 16,
64
+ borderRadius: 16,
65
+ borderWidth: 1,
66
+ gap: 14,
67
+ },
68
+ avatar: {
69
+ width: 64,
70
+ height: 64,
71
+ borderRadius: 32,
72
+ backgroundColor: '#1A1A24',
73
+ },
74
+ info: {
75
+ flex: 1,
76
+ gap: 2,
77
+ },
78
+ username: {
79
+ fontSize: 18,
80
+ fontWeight: '700',
81
+ },
82
+ address: {
83
+ fontSize: 14,
84
+ fontFamily: 'monospace',
85
+ },
86
+ memberSince: {
87
+ fontSize: 12,
88
+ marginTop: 2,
89
+ },
90
+ });
@@ -0,0 +1,8 @@
1
+ export { ConnectWalletScreen } from './ConnectWalletScreen';
2
+ export type { ConnectWalletScreenProps } from './ConnectWalletScreen';
3
+ export { UserProfileCard } from './UserProfileCard';
4
+ export type { UserProfileCardProps } from './UserProfileCard';
5
+ export { SettingsSheet } from './SettingsSheet';
6
+ export type { SettingsSheetProps } from './SettingsSheet';
7
+ export { useDubsTheme } from './theme';
8
+ export type { DubsTheme } from './theme';
@@ -0,0 +1,57 @@
1
+ import { useColorScheme } from 'react-native';
2
+
3
+ export interface DubsTheme {
4
+ background: string;
5
+ surface: string;
6
+ surfaceActive: string;
7
+ border: string;
8
+ text: string;
9
+ textSecondary: string;
10
+ textMuted: string;
11
+ textDim: string;
12
+ accent: string;
13
+ success: string;
14
+ live: string;
15
+ errorText: string;
16
+ errorBg: string;
17
+ errorBorder: string;
18
+ }
19
+
20
+ const dark: DubsTheme = {
21
+ background: '#08080D',
22
+ surface: '#111118',
23
+ surfaceActive: '#7C3AED',
24
+ border: '#1A1A24',
25
+ text: '#FFFFFF',
26
+ textSecondary: '#E0E0EE',
27
+ textMuted: '#666666',
28
+ textDim: '#555555',
29
+ accent: '#7C3AED',
30
+ success: '#22C55E',
31
+ live: '#EF4444',
32
+ errorText: '#F87171',
33
+ errorBg: '#1A0A0A',
34
+ errorBorder: '#3A1515',
35
+ };
36
+
37
+ const light: DubsTheme = {
38
+ background: '#FFFFFF',
39
+ surface: '#F0F0F5',
40
+ surfaceActive: '#7C3AED',
41
+ border: '#E0E0E8',
42
+ text: '#111118',
43
+ textSecondary: '#333333',
44
+ textMuted: '#888888',
45
+ textDim: '#999999',
46
+ accent: '#7C3AED',
47
+ success: '#16A34A',
48
+ live: '#DC2626',
49
+ errorText: '#DC2626',
50
+ errorBg: '#FEF2F2',
51
+ errorBorder: '#FECACA',
52
+ };
53
+
54
+ export function useDubsTheme(): DubsTheme {
55
+ const scheme = useColorScheme();
56
+ return scheme === 'light' ? light : dark;
57
+ }
@@ -0,0 +1,67 @@
1
+ import { Connection, Transaction } from '@solana/web3.js';
2
+ import type { TransactionConfirmationStatus } from '@solana/web3.js';
3
+ import type { WalletAdapter } from '../wallet/types';
4
+
5
+ /**
6
+ * Deserialize a base64-encoded transaction, sign via wallet adapter, send to Solana.
7
+ * Returns the transaction signature.
8
+ */
9
+ export async function signAndSendBase64Transaction(
10
+ base64Tx: string,
11
+ wallet: WalletAdapter,
12
+ connection: Connection,
13
+ ): Promise<string> {
14
+ if (!wallet.publicKey) throw new Error('Wallet not connected');
15
+
16
+ const txBuffer = Buffer.from(base64Tx, 'base64');
17
+ const transaction = Transaction.from(txBuffer);
18
+
19
+ // If the wallet supports signAndSend in one step, prefer that
20
+ if (wallet.signAndSendTransaction) {
21
+ return wallet.signAndSendTransaction(transaction);
22
+ }
23
+
24
+ // Otherwise: sign, then send via RPC
25
+ const signed = await wallet.signTransaction(transaction);
26
+ const signature = await connection.sendRawTransaction(signed.serialize(), {
27
+ skipPreflight: true,
28
+ });
29
+
30
+ return signature;
31
+ }
32
+
33
+ /**
34
+ * Poll for transaction confirmation using getSignatureStatuses.
35
+ * Uses polling instead of deprecated WebSocket-based confirmTransaction.
36
+ */
37
+ export async function pollTransactionConfirmation(
38
+ signature: string,
39
+ connection: Connection,
40
+ commitment: TransactionConfirmationStatus = 'confirmed',
41
+ timeout: number = 60000,
42
+ interval: number = 1500,
43
+ ): Promise<void> {
44
+ const start = Date.now();
45
+ const confirmationOrder: TransactionConfirmationStatus[] = ['processed', 'confirmed', 'finalized'];
46
+ const targetIndex = confirmationOrder.indexOf(commitment);
47
+
48
+ while (Date.now() - start < timeout) {
49
+ const statuses = await connection.getSignatureStatuses([signature]);
50
+ const status = statuses?.value?.[0];
51
+
52
+ if (status?.err) {
53
+ throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
54
+ }
55
+
56
+ if (status?.confirmationStatus) {
57
+ const currentIndex = confirmationOrder.indexOf(status.confirmationStatus);
58
+ if (currentIndex >= targetIndex) {
59
+ return;
60
+ }
61
+ }
62
+
63
+ await new Promise(resolve => setTimeout(resolve, interval));
64
+ }
65
+
66
+ throw new Error(`Transaction confirmation timeout after ${timeout}ms`);
67
+ }
@@ -0,0 +1,3 @@
1
+ export type { WalletAdapter } from './types';
2
+ export { MwaWalletAdapter } from './mwa-adapter';
3
+ export type { MwaAdapterConfig, MwaTransactFn } from './mwa-adapter';
@@ -0,0 +1,156 @@
1
+ import { PublicKey, Transaction } from '@solana/web3.js';
2
+ import type { WalletAdapter } from './types';
3
+
4
+ /** Convert an MWA address (base64 string or Uint8Array) to a PublicKey */
5
+ function toPublicKey(address: string | Uint8Array): PublicKey {
6
+ if (address instanceof Uint8Array) {
7
+ return new PublicKey(address);
8
+ }
9
+ // MWA protocol returns addresses as base64 — decode to bytes
10
+ const bytes = Uint8Array.from(atob(address), (c) => c.charCodeAt(0));
11
+ return new PublicKey(bytes);
12
+ }
13
+
14
+ /** The `transact` function signature from @solana-mobile/mobile-wallet-adapter-protocol-web3js */
15
+ export type MwaTransactFn = (
16
+ callback: (wallet: any) => Promise<any>,
17
+ ) => Promise<any>;
18
+
19
+ export interface MwaAdapterConfig {
20
+ /** The MWA `transact` function — import it from @solana-mobile/mobile-wallet-adapter-protocol-web3js */
21
+ transact: MwaTransactFn;
22
+ appIdentity: {
23
+ name: string;
24
+ uri?: string;
25
+ icon?: string;
26
+ };
27
+ cluster?: string;
28
+ onAuthTokenChange?: (token: string | null) => void;
29
+ }
30
+
31
+ /**
32
+ * Mobile Wallet Adapter implementation.
33
+ * Wraps @solana-mobile/mobile-wallet-adapter-protocol-web3js.
34
+ *
35
+ * Usage:
36
+ * ```ts
37
+ * import { transact } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
38
+ *
39
+ * const adapter = new MwaWalletAdapter({
40
+ * transact,
41
+ * appIdentity: { name: 'My App' },
42
+ * });
43
+ * ```
44
+ */
45
+ export class MwaWalletAdapter implements WalletAdapter {
46
+ private _publicKey: PublicKey | null = null;
47
+ private _connected = false;
48
+ private _authToken: string | null = null;
49
+ private readonly config: MwaAdapterConfig;
50
+ private readonly transact: MwaTransactFn;
51
+
52
+ constructor(config: MwaAdapterConfig) {
53
+ this.config = config;
54
+ this.transact = config.transact;
55
+ }
56
+
57
+ get publicKey(): PublicKey | null {
58
+ return this._publicKey;
59
+ }
60
+
61
+ get connected(): boolean {
62
+ return this._connected;
63
+ }
64
+
65
+ /**
66
+ * Connect to a mobile wallet. Call this before any signing.
67
+ */
68
+ async connect(): Promise<void> {
69
+ await this.transact(async (wallet) => {
70
+ const authResult = this._authToken
71
+ ? await wallet.reauthorize({ auth_token: this._authToken })
72
+ : await wallet.authorize({
73
+ identity: this.config.appIdentity,
74
+ cluster: this.config.cluster || 'mainnet-beta',
75
+ });
76
+
77
+ this._publicKey = toPublicKey(authResult.accounts[0].address);
78
+ this._authToken = authResult.auth_token;
79
+ this._connected = true;
80
+
81
+ this.config.onAuthTokenChange?.(this._authToken);
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Disconnect and clear auth token.
87
+ */
88
+ disconnect(): void {
89
+ this._publicKey = null;
90
+ this._connected = false;
91
+ this._authToken = null;
92
+ this.config.onAuthTokenChange?.(null);
93
+ }
94
+
95
+ async signTransaction(transaction: Transaction): Promise<Transaction> {
96
+ if (!this._connected) throw new Error('Wallet not connected');
97
+
98
+ const signed = await this.transact(async (wallet) => {
99
+ const reauth = await wallet.reauthorize({ auth_token: this._authToken });
100
+ this._authToken = reauth.auth_token;
101
+ this.config.onAuthTokenChange?.(this._authToken);
102
+
103
+ const result = await wallet.signTransactions({
104
+ transactions: [transaction],
105
+ });
106
+ return result[0];
107
+ });
108
+
109
+ return signed;
110
+ }
111
+
112
+ async signMessage(message: Uint8Array): Promise<Uint8Array> {
113
+ if (!this._connected || !this._publicKey) throw new Error('Wallet not connected');
114
+
115
+ const sig = await this.transact(async (wallet) => {
116
+ const reauth = await wallet.reauthorize({ auth_token: this._authToken });
117
+ this._authToken = reauth.auth_token;
118
+ this.config.onAuthTokenChange?.(this._authToken);
119
+
120
+ const result = await wallet.signMessages({
121
+ addresses: [this._publicKey!.toBytes()],
122
+ payloads: [message],
123
+ });
124
+ return result[0];
125
+ });
126
+
127
+ return sig instanceof Uint8Array ? sig : new Uint8Array(sig);
128
+ }
129
+
130
+ async signAndSendTransaction(transaction: Transaction): Promise<string> {
131
+ if (!this._connected) throw new Error('Wallet not connected');
132
+
133
+ const signature = await this.transact(async (wallet) => {
134
+ const reauth = await wallet.reauthorize({ auth_token: this._authToken });
135
+ this._authToken = reauth.auth_token;
136
+ this.config.onAuthTokenChange?.(this._authToken);
137
+
138
+ const result = await wallet.signAndSendTransactions({
139
+ transactions: [transaction],
140
+ });
141
+ return result[0];
142
+ });
143
+
144
+ // MWA returns Uint8Array signature — convert to base58 string
145
+ if (signature instanceof Uint8Array) {
146
+ const bs58 = await import('@solana/web3.js').then(() => {
147
+ return new PublicKey(signature).toBase58();
148
+ }).catch(() => {
149
+ return Buffer.from(signature).toString('base64');
150
+ });
151
+ return bs58;
152
+ }
153
+
154
+ return String(signature);
155
+ }
156
+ }
@@ -0,0 +1,30 @@
1
+ import type { Transaction, PublicKey } from '@solana/web3.js';
2
+
3
+ /**
4
+ * Minimal wallet adapter interface.
5
+ * Two required methods: publicKey + signTransaction.
6
+ * signAndSendTransaction is optional — if not provided, the SDK signs then sends via RPC.
7
+ */
8
+ export interface WalletAdapter {
9
+ /** The connected wallet's public key, or null if not connected */
10
+ publicKey: PublicKey | null;
11
+
12
+ /** Whether the wallet is currently connected */
13
+ connected: boolean;
14
+
15
+ /** Sign a transaction without sending it */
16
+ signTransaction(transaction: Transaction): Promise<Transaction>;
17
+
18
+ /**
19
+ * Optional: Sign and send in one step.
20
+ * If provided, the SDK will prefer this over signTransaction + sendRawTransaction.
21
+ * Returns the transaction signature.
22
+ */
23
+ signAndSendTransaction?(transaction: Transaction): Promise<string>;
24
+
25
+ /**
26
+ * Optional: Sign an arbitrary message (for authentication).
27
+ * Returns the signature as a Uint8Array.
28
+ */
29
+ signMessage?(message: Uint8Array): Promise<Uint8Array>;
30
+ }