Skip to content

Commit

Permalink
Return a wallet icon in authorize result (#906)
Browse files Browse the repository at this point in the history
* wallet icon

* doc

* fix tests

* oops missed file in previous commit

* make icon optional

* add checks for wallet icon data uri

* fully deprecate AuthorizedAccount.icon

* fix tests
  • Loading branch information
Funkatronics authored Aug 2, 2024
1 parent 770f913 commit 04c5cdd
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ public static class AuthorizationResult {
public final String accountLabel;
@Nullable
public final Uri walletUriBase;
@Nullable
public final Uri walletIcon;
@NonNull @Size(min = 1)
public final AuthorizedAccount[] accounts;
@Nullable
Expand All @@ -153,8 +155,10 @@ public static class AuthorizationResult {
private AuthorizationResult(@NonNull String authToken,
@NonNull @Size(min = 1) AuthorizedAccount[] accounts,
@Nullable Uri walletUriBase,
@Nullable Uri walletIcon,
@Nullable SignInResult signInResult) {
this.authToken = authToken;
this.walletIcon = walletIcon;
this.walletUriBase = walletUriBase;
this.accounts = accounts;
this.signInResult = signInResult;
Expand All @@ -174,7 +178,7 @@ public String toString() {

@NonNull
public AuthorizationResult with(SignInResult signInResult) {
return new AuthorizationResult(authToken, accounts, walletUriBase, signInResult);
return new AuthorizationResult(authToken, accounts, walletUriBase, walletIcon, signInResult);
}

public static class AuthorizedAccount {
Expand Down Expand Up @@ -247,7 +251,7 @@ public static AuthorizationResult create(
Uri walletUriBase
) {
AuthorizedAccount[] accounts = new AuthorizedAccount[] { new AuthorizedAccount(publicKey, accountLabel, null, null) };
return new AuthorizationResult(authToken, accounts, walletUriBase, null);
return new AuthorizationResult(authToken, accounts, walletUriBase, null, null);
}

@TestOnly @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
Expand All @@ -256,7 +260,7 @@ public static AuthorizationResult create(
AuthorizedAccount[] accounts,
Uri walletUriBase
) {
return new AuthorizationResult(authToken, accounts, walletUriBase, null);
return new AuthorizationResult(authToken, accounts, walletUriBase, null, null);
}
}

Expand Down Expand Up @@ -331,6 +335,10 @@ protected AuthorizationResult processResult(@Nullable Object o)
walletUriBaseStr + "'; expected a 'https' URI");
}

final String walletIconStr = jo.has(ProtocolContract.RESULT_WALLET_ICON) ?
jo.optString(ProtocolContract.RESULT_WALLET_ICON) : null;
final Uri walletIcon = walletIconStr != null ? Uri.parse(walletIconStr) : null;

final JSONObject signInResultJson = jo.has(ProtocolContract.RESULT_SIGN_IN) ?
jo.optJSONObject(ProtocolContract.RESULT_SIGN_IN) : null;
final AuthorizationResult.SignInResult signInResult;
Expand Down Expand Up @@ -360,7 +368,7 @@ protected AuthorizationResult processResult(@Nullable Object o)
signInResult = null;
}

return new AuthorizationResult(authToken, authorizedAccounts, walletUriBase, signInResult);
return new AuthorizationResult(authToken, authorizedAccounts, walletUriBase, walletIcon, signInResult);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public class ProtocolContract {
// RESULT_ACCOUNTS optionally includes a RESULT_SUPPORTED_FEATURES

public static final String RESULT_WALLET_URI_BASE = "wallet_uri_base"; // type: String (absolute URI)
public static final String RESULT_WALLET_ICON = "wallet_icon"; // type: String (data URI)

public static final String RESULT_SIGN_IN = "sign_in_result"; // type JSON object
public static final String RESULT_SIGN_IN_ADDRESS = "address"; // type: String (address)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,9 +410,9 @@ public AuthRecord issue(@NonNull String name,
// If no matching account exists, create one
if (accountRecordQueried == null) {
accountId = (int) mAccountsDao.insert(authRecordId, account.publicKey,
account.accountLabel, account.icon, account.chains, account.features);
account.accountLabel, account.accountIcon, account.chains, account.features);
accountRecord = new AccountRecord(accountId, authRecordId, account.publicKey,
account.accountLabel, account.icon, account.chains, account.features);
account.accountLabel, account.accountIcon, account.chains, account.features);
} else {
accountRecord = accountRecordQueried;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,14 @@ private void onAuthorizationComplete(@NonNull NotifyOnCompleteFuture<Authorizati
if (aa.accountLabel != null) {
account.put(ProtocolContract.RESULT_ACCOUNTS_LABEL, aa.accountLabel);
}
if (aa.icon != null) {
account.put(ProtocolContract.RESULT_ACCOUNTS_ICON, aa.icon);
if (aa.accountIcon != null) {
account.put(ProtocolContract.RESULT_ACCOUNTS_ICON, aa.accountIcon);
}
accounts.put(account);
}
o.put(ProtocolContract.RESULT_ACCOUNTS, accounts);
o.put(ProtocolContract.RESULT_WALLET_URI_BASE, result.walletUriBase); // OK if null
o.put(ProtocolContract.RESULT_WALLET_ICON, result.walletIcon);
if (result.signInResult != null) {
final JSONObject signInResultJson = new JSONObject();
final String address = Base64.encodeToString(result.signInResult.publicKey, Base64.NO_WRAP);
Expand Down Expand Up @@ -351,6 +352,9 @@ public static class AuthorizationResult {
@Nullable
public final Uri walletUriBase;

@Nullable
public final Uri walletIcon;

@Deprecated
@NonNull
public final AuthorizedAccount account;
Expand Down Expand Up @@ -378,17 +382,32 @@ public AuthorizationResult(@NonNull String authToken,
this(authToken, new AuthorizedAccount[] { account }, walletUriBase, signInResult);
}

@Deprecated
public AuthorizationResult(@NonNull String authToken,
@NonNull @Size(min = 1) AuthorizedAccount[] accounts,
@Nullable Uri walletUriBase,
@Nullable SignInResult signInResult) {
this(authToken, accounts, walletUriBase, null, signInResult);
}

public AuthorizationResult(@NonNull String authToken,
@NonNull @Size(min = 1) AuthorizedAccount[] accounts,
@Nullable Uri walletUriBase,
@Nullable Uri walletIcon,
@Nullable SignInResult signInResult) {
this.authToken = authToken;
this.walletUriBase = walletUriBase;
this.accounts = accounts;
this.account = accounts[0];
this.signInResult = signInResult;
this.publicKey = account.publicKey;
this.accountLabel = account.accountLabel;
if (walletIcon != null
&& walletIcon.getScheme() != null && walletIcon.getScheme().equals("data")) {
this.walletIcon = walletIcon;
} else {
throw new IllegalArgumentException("wallet icon URI must be a data URI");
}
}

@NonNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,32 @@ public class AuthorizedAccount {
@Nullable
public final String accountLabel;
@Nullable
public final Uri icon;
public final Uri accountIcon;
@Nullable
public final String[] chains;
@Nullable
public final String[] features;

/**
* The account icon URI
* @deprecated
* Use {@link AuthorizedAccount#accountIcon} instead
*/
@Deprecated
@Nullable
public final Uri icon;

public AuthorizedAccount(@NonNull byte[] publicKey,
@Nullable String accountLabel,
@Nullable Uri icon,
@Nullable Uri accountIcon,
@Nullable String[] chains,
@Nullable String[] features) {
this.publicKey = publicKey;
this.displayAddress = null;
this.displayAddressFormat = null;
this.accountLabel = accountLabel;
this.icon = icon;
this.accountIcon = accountIcon;
this.icon = accountIcon;
this.chains = chains;
this.features = features;
}
Expand All @@ -45,14 +55,15 @@ public AuthorizedAccount(@NonNull byte[] publicKey,
@Nullable String displayAddress,
@Nullable String displayAddressFormat,
@Nullable String accountLabel,
@Nullable Uri icon,
@Nullable Uri accountIcon,
@Nullable String[] chains,
@Nullable String[] features) {
this.publicKey = publicKey;
this.displayAddress = displayAddress;
this.displayAddressFormat = displayAddressFormat;
this.accountLabel = accountLabel;
this.icon = icon;
this.accountIcon = accountIcon;
this.icon = accountIcon;
this.chains = chains;
this.features = features;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.solana.mobilewalletadapter.walletlib.scenario;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.Base64;

import java.io.ByteArrayOutputStream;

public class DefaultWalletIconProvider implements WalletIconProvider{
private final Drawable mIconDrawable;
public DefaultWalletIconProvider(Context context) {
this.mIconDrawable = context.getPackageManager().getApplicationIcon(context.getApplicationInfo());
}

@Override
public Uri getWalletIconDataUri() {
int width = mIconDrawable.getIntrinsicWidth();
int height = mIconDrawable.getIntrinsicHeight();
Bitmap iconBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mIconDrawable.setBounds(0, 0, width, height);
mIconDrawable.draw(new Canvas(iconBitmap));
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
iconBitmap.compress(Bitmap.CompressFormat.PNG, 100, byteStream);
return Uri.parse("data:image/png;base64," + Base64.encodeToString(byteStream.toByteArray(), Base64.DEFAULT));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ public abstract class LocalScenario implements Scenario {

private final PowerConfigProvider mPowerManager;

private final Uri mWalletIcon;

@Nullable
private ScheduledFuture<?> mNoConnectionTimeoutHandler;
private final ScheduledExecutorService mTimeoutExecutorService =
Expand All @@ -84,6 +86,18 @@ protected LocalScenario(@NonNull Context context,
@NonNull byte[] associationPublicKey,
@NonNull PowerConfigProvider powerConfigProvider,
@NonNull List<SessionProperties.ProtocolVersion> associationProtocolVersions) {
this(context, mobileWalletAdapterConfig, authIssuerConfig, callbacks, associationPublicKey,
powerConfigProvider, associationProtocolVersions, new DefaultWalletIconProvider(context));
}

/*package*/ LocalScenario(@NonNull Context context,
@NonNull MobileWalletAdapterConfig mobileWalletAdapterConfig,
@NonNull AuthIssuerConfig authIssuerConfig,
@NonNull Callbacks callbacks,
@NonNull byte[] associationPublicKey,
@NonNull PowerConfigProvider powerConfigProvider,
@NonNull List<SessionProperties.ProtocolVersion> associationProtocolVersions,
@NonNull WalletIconProvider iconProvider) {
mCallbacks = callbacks;
mMobileWalletAdapterConfig = mobileWalletAdapterConfig;
this.associationProtocolVersions = associationProtocolVersions;
Expand All @@ -97,6 +111,15 @@ protected LocalScenario(@NonNull Context context,
mAuthRepository = new AuthRepositoryImpl(context, authIssuerConfig);

this.mPowerManager = powerConfigProvider;

Uri walletIcon = iconProvider.getWalletIconDataUri();
if (walletIcon == null ||
(walletIcon.getScheme() != null && walletIcon.getScheme().equals("data"))) {
this.mWalletIcon = walletIcon;
} else {
throw new IllegalArgumentException("wallet icon provider returned an invalid icon URI: " +
"the wallet icon must be a data URI");
}
}

@Override
Expand Down Expand Up @@ -230,7 +253,7 @@ public void authorize(@NonNull MobileWalletAdapterServer.AuthorizeRequest reques

final String authToken = mAuthRepository.toAuthToken(authRecord);
request.complete(new MobileWalletAdapterServer.AuthorizationResult(authToken,
authorize.accounts, authorize.walletUriBase, authorize.signInResult));
authorize.accounts, authorize.walletUriBase, mWalletIcon, authorize.signInResult));
} else {
request.completeExceptionally(new MobileWalletAdapterServer.RequestDeclinedException(
"authorize request declined"));
Expand Down Expand Up @@ -304,7 +327,7 @@ private void doReauthorize(@NonNull MobileWalletAdapterServer.AuthorizeRequest r
mIoHandler.post(() -> request.complete(
new MobileWalletAdapterServer.AuthorizationResult(
authToken, authRecord.getAuthorizedAccounts(),
authRecord.walletUriBase, null)));
authRecord.walletUriBase, mWalletIcon, null)));
} catch (ExecutionException e) {
final Throwable cause = e.getCause();
assert(cause instanceof Exception); // expected to always be an Exception
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ associationPublicKey, port, new DevicePowerConfigProvider(context),
this.mWebSocketServer = new LocalWebSocketServer(this, mWebSocketServerCallbacks);
}

/*package*/ LocalWebSocketServerScenario(@NonNull Context context,
@NonNull MobileWalletAdapterConfig mobileWalletAdapterConfig,
@NonNull AuthIssuerConfig authIssuerConfig,
@NonNull LocalScenario.Callbacks callbacks,
@NonNull byte[] associationPublicKey,
@WebSocketsTransportContract.LocalPortRange int port,
PowerConfigProvider powerConfigProvider,
@NonNull List<SessionProperties.ProtocolVersion> associationProtocolVersions,
@NonNull WalletIconProvider iconProvider) {
super(context, mobileWalletAdapterConfig, authIssuerConfig, callbacks, associationPublicKey,
powerConfigProvider, associationProtocolVersions, iconProvider);
this.port = port;
this.mWebSocketServer = new LocalWebSocketServer(this, mWebSocketServerCallbacks);
}

@Override
public void start() {
if (mState != State.NOT_STARTED) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.solana.mobilewalletadapter.walletlib.scenario;

import android.net.Uri;

import androidx.annotation.Nullable;

/*package*/ interface WalletIconProvider {
/**
* Returns a base64 encoded data URI of the wallet icon. This will be the icon dapps display
* for your wallet. The wallet icon must be an SVG, PNG, WebP, or GIF image.
* <p>
* You can use a tool like <a href="https://base64.guru/converter/encode/image">https://base64.guru/converter/encode/image</a>
* to encode an image using the "Data URI" setting. It's a good idea to compress your image
* losslessly with a tool like <a href="https://imageoptim.com">https://imageoptim.com</a> first.
*
* @return a base64 encoded data URI of an SVG, PNG, WebP, or GIF image, or null
*/
@Nullable Uri getWalletIconDataUri();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.junit.Assert.*;

import android.content.Context;
import android.net.Uri;

import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
import androidx.test.core.app.ApplicationProvider;
Expand Down Expand Up @@ -56,13 +57,14 @@ public void onLowPowerAndNoConnection() {
};

PowerConfigProvider powerConfig = () -> false;
WalletIconProvider iconProvider = () -> null;

List<SessionProperties.ProtocolVersion> supportedVersions =
List.of(SessionProperties.ProtocolVersion.LEGACY);

// when
new LocalWebSocketServerScenario(context, config, authConfig, lowPowerNoConnectionCallback,
publicKey, port, powerConfig, supportedVersions).start();
publicKey, port, powerConfig, supportedVersions, iconProvider).start();
boolean lowPowerNoConnectionCallbackFired = latch.await(200, TimeUnit.MILLISECONDS);

// then
Expand Down Expand Up @@ -98,13 +100,14 @@ public void onLowPowerAndNoConnection() {
};

PowerConfigProvider powerConfig = () -> true;
WalletIconProvider iconProvider = () -> null;

List<SessionProperties.ProtocolVersion> supportedVersions =
List.of(SessionProperties.ProtocolVersion.LEGACY);

// when
new LocalWebSocketServerScenario(context, config, authConfig, lowPowerNoConnectionCallback,
publicKey, port, powerConfig, supportedVersions).start();
publicKey, port, powerConfig, supportedVersions, iconProvider).start();
boolean lowPowerNoConnectionCallbackFired = latch.await(200, TimeUnit.MILLISECONDS);

// then
Expand Down

0 comments on commit 04c5cdd

Please sign in to comment.