diff --git a/.gitignore b/.gitignore index c21e214d..22ac5d86 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ buck-out/ # Other Files app/google-services.json +app/twitter.properties *.log .vagrant *.hprof diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 33fca8b4..310c930a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,7 +11,7 @@ build apk: - apt-get -y update && apt-get -y install build-essential ca-certificates curl git gpg-agent openjdk-8-jdk software-properties-common wget zipalign - chmod u+x $CI_PROJECT_DIR/gradlew - export ANDROID_SDK_ROOT=~/.buildozer/android/platform/android-sdk-23 - - export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -q printVersionName --console=plain | tail -1) + - export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1) artifacts: paths: - bin/browser-*-release__arm.apk @@ -27,8 +27,8 @@ build apk: - yarn - chmod u+x ./release.sh - ./release.sh - - cp bin/browser-$BUILD_VERSION-release__arm.apk /dev/null - - cp bin/browser-$BUILD_VERSION-release__arm64.apk /dev/null + - cp bin/browser-$BUILD_VERSION-release__arm.apk $CI_PROJECT_DIR + - cp bin/browser-$BUILD_VERSION-release__arm64.apk $CI_PROJECT_DIR deploy build.lbry.io: image: python:stretch @@ -39,7 +39,7 @@ deploy build.lbry.io: - apt-get -y update && apt-get -y install openjdk-8-jdk - pip install awscli - chmod u+x $CI_PROJECT_DIR/gradlew - - export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -q printVersionName --console=plain | tail -1) + - export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1) - export BUILD_APK_FILENAME__32=browser-$BUILD_VERSION-release__arm.apk - export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk script: @@ -58,7 +58,7 @@ release apk: - apt-get -y update && apt-get -y install openjdk-8-jdk - pip install awscli githubrelease - chmod u+x $CI_PROJECT_DIR/gradlew - - export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -q printVersionName --console=plain | tail -1) + - export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1) - export BUILD_APK_FILENAME__32=browser-$BUILD_VERSION-release__arm.apk - export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk script: diff --git a/.gitsecret/keys/pubring.kbx b/.gitsecret/keys/pubring.kbx index b5b2e868..e16cdd1b 100644 Binary files a/.gitsecret/keys/pubring.kbx and b/.gitsecret/keys/pubring.kbx differ diff --git a/.gitsecret/keys/pubring.kbx~ b/.gitsecret/keys/pubring.kbx~ index 64baf5a6..b5b2e868 100644 Binary files a/.gitsecret/keys/pubring.kbx~ and b/.gitsecret/keys/pubring.kbx~ differ diff --git a/.gitsecret/keys/random_seed b/.gitsecret/keys/random_seed index 86bb2f48..3708c629 100644 Binary files a/.gitsecret/keys/random_seed and b/.gitsecret/keys/random_seed differ diff --git a/.gitsecret/paths/mapping.cfg b/.gitsecret/paths/mapping.cfg index 3aff0c5a..5d9c956c 100644 --- a/.gitsecret/paths/mapping.cfg +++ b/.gitsecret/paths/mapping.cfg @@ -1,2 +1,3 @@ lbry-android.keystore:0d958c531870694624cc877ea98ca1c583485f8ebbb3a5acca58b1930c190d65 app/google-services.json:896a0bee8294a36d061f10fa926129d8a780528b34d0a2f03113400c4246d67c +app/twitter.properties:01212d70712f2041efb5c814bf30ecbf6f72e1ca5179c7647c4f8cbd995dd033 diff --git a/app/build.gradle b/app/build.gradle index 3b8e12de..7a0542a4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,12 @@ import com.google.gms.googleservices.GoogleServicesPlugin +Properties twitterProps = new Properties() +try { + twitterProps.load(project.file('twitter.properties').newDataInputStream()) +} catch (Exception ex) { + throw new GradleException("Missing twitter.properties.") +} + apply plugin: 'com.android.application' android { @@ -22,6 +29,10 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + packagingOptions { + exclude 'META-INF/DEPENDENCIES' + } + productFlavors { __32bit { versionCode android.defaultConfig.versionCode * 10 + 1 @@ -38,7 +49,13 @@ android { } buildTypes { + debug { + resValue "string", "TWITTER_CONSUMER_KEY", "\"${twitterProps.getProperty("twitterConsumerKey")}\"" + resValue "string", "TWITTER_CONSUMER_SECRET", "\"${twitterProps.getProperty("twitterConsumerSecret")}\"" + } release { + resValue "string", "TWITTER_CONSUMER_KEY", "\"${twitterProps.getProperty("twitterConsumerKey")}\"" + resValue "string", "TWITTER_CONSUMER_SECRET", "\"${twitterProps.getProperty("twitterConsumerSecret")}\"" minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } @@ -51,6 +68,13 @@ task printVersionName { } } +configurations { + all { + exclude module: 'httpclient' + exclude module: 'commons-logging' + } +} + dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) @@ -76,6 +100,9 @@ dependencies { implementation 'com.google.firebase:firebase-analytics:17.4.2' implementation 'com.google.android.gms:play-services-base:17.2.1' implementation 'com.google.firebase:firebase-messaging:20.2.0' + implementation 'com.google.oauth-client:google-oauth-client:1.30.4' + + implementation 'com.android.billingclient:billing:3.0.0' implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.google.android.exoplayer:exoplayer-core:2.11.4' diff --git a/app/google-services.json.secret b/app/google-services.json.secret index 1e09a2e1..ada7f6f6 100644 Binary files a/app/google-services.json.secret and b/app/google-services.json.secret differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f7d535e9..145b4940 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + specialRouteFragmentClassMap; @Getter private boolean inPictureInPictureMode; @@ -213,7 +224,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener public static boolean startingPermissionRequest = false; public static boolean startingSignInFlowActivity = false; - + private BillingClient billingClient; @Getter private boolean enteringPIPMode = false; private boolean fullSyncInProgress = false; @@ -408,6 +419,24 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); + // setup the billing client in main activity (to handle cases where the verification purchase flow may have been interrupted) + billingClient = BillingClient.newBuilder(this) + .setListener(new PurchasesUpdatedListener() { + @Override + public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List purchases) { + int responseCode = billingResult.getResponseCode(); + if (responseCode == BillingClient.BillingResponseCode.OK && purchases != null) + { + for (Purchase purchase : purchases) { + handlePurchase(purchase); + } + } + } + }) + .enablePendingPurchases() + .build(); + establishBillingClientConnection(); + playerNotificationManager = new PlayerNotificationManager( this, LbrynetService.NOTIFICATION_CHANNEL_ID, PLAYBACK_NOTIFICATION_ID, new PlayerNotificationDescriptionAdapter()); @@ -1024,6 +1053,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener @Override protected void onResume() { super.onResume(); + checkPurchases(); enteringPIPMode = false; applyNavbarSigninPadding(); @@ -1046,6 +1076,33 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener }*/ } + private void checkPurchases() { + if (billingClient != null) { + Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.INAPP); + if (result.getPurchasesList() != null) { + for (Purchase purchase : result.getPurchasesList()) { + handlePurchase(purchase); + } + } + } + } + + private void handlePurchase(Purchase purchase) { + handleBillingPurchase(purchase, billingClient, MainActivity.this, null, new RewardVerifiedHandler() { + @Override + public void onSuccess(RewardVerified rewardVerified) { + if (Lbryio.currentUser != null) { + Lbryio.currentUser.setRewardApproved(rewardVerified.isRewardApproved()); + } + } + + @Override + public void onError(Exception error) { + // pass + } + }); + } + private void checkPendingOpens() { if (pendingFollowingReload) { loadFollowingContent(); @@ -2536,8 +2593,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, true); } } catch (Exception ex) { - // nope - android.util.Log.e(TAG, String.format("App startup failed: %s", ex.getMessage()), ex); + // nopecd + Log.e(TAG, String.format("App startup failed: %s", ex.getMessage()), ex); return false; } finally { Helper.closeCloseable(reader); @@ -3269,6 +3326,53 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener return (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED); } + private void establishBillingClientConnection() { + if (billingClient != null) { + billingClient.startConnection(new BillingClientStateListener() { + @Override + public void onBillingSetupFinished(@NonNull BillingResult billingResult) { + if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { + // no need to do anything here. purchases are always checked server-side + checkPurchases(); + } + } + + @Override + public void onBillingServiceDisconnected() { + establishBillingClientConnection(); + } + }); + } + } + + public static void handleBillingPurchase( + Purchase purchase, + BillingClient billingClient, + Context context, + View progressView, + RewardVerifiedHandler handler) { + String sku = purchase.getSku(); + if (SKU_SKIP.equalsIgnoreCase(sku)) { + // send purchase token for verification + if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED + /*&& isSignatureValid(purchase)*/) { + // consume the purchase + String purchaseToken = purchase.getPurchaseToken(); + ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken).build(); + billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() { + @Override + public void onConsumeResponse(@NonNull BillingResult billingResult, @NonNull String s) { + + } + }); + + // send the purchase token to the backend to complete verification + AndroidPurchaseTask task = new AndroidPurchaseTask(purchaseToken, progressView, context, handler); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + } + } + public interface BackPressInterceptor { boolean onBackPressed(); } diff --git a/app/src/main/java/io/lbry/browser/VerificationActivity.java b/app/src/main/java/io/lbry/browser/VerificationActivity.java index f332b25d..2950dd3b 100644 --- a/app/src/main/java/io/lbry/browser/VerificationActivity.java +++ b/app/src/main/java/io/lbry/browser/VerificationActivity.java @@ -7,19 +7,35 @@ import android.content.IntentFilter; import android.graphics.Color; import android.os.AsyncTask; import android.os.Bundle; +import android.os.Handler; import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; import androidx.viewpager2.widget.ViewPager2; +import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.BillingClientStateListener; +import com.android.billingclient.api.BillingFlowParams; +import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.Purchase; +import com.android.billingclient.api.PurchasesUpdatedListener; +import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.SkuDetailsParams; +import com.android.billingclient.api.SkuDetailsResponseListener; import com.google.android.material.snackbar.Snackbar; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import io.lbry.browser.adapter.VerificationPagerAdapter; import io.lbry.browser.listener.SignInListener; import io.lbry.browser.listener.WalletSyncListener; +import io.lbry.browser.model.lbryinc.RewardVerified; import io.lbry.browser.model.lbryinc.User; +import io.lbry.browser.tasks.RewardVerifiedHandler; import io.lbry.browser.tasks.lbryinc.FetchCurrentUserTask; import io.lbry.browser.utils.Helper; import io.lbry.browser.utils.LbryAnalytics; @@ -32,11 +48,59 @@ public class VerificationActivity extends FragmentActivity implements SignInList public static final int VERIFICATION_FLOW_REWARDS = 2; public static final int VERIFICATION_FLOW_WALLET = 3; + private BillingClient billingClient; private BroadcastReceiver sdkReceiver; private String email; private boolean signedIn; private int flow; + private PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() { + @Override + public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List purchases) { + int responseCode = billingResult.getResponseCode(); + if (responseCode == BillingClient.BillingResponseCode.OK && purchases != null) + { + for (Purchase purchase : purchases) { + if (MainActivity.SKU_SKIP.equalsIgnoreCase(purchase.getSku())) { + showLoading(); + MainActivity.handleBillingPurchase( + purchase, + billingClient, + VerificationActivity.this, null, new RewardVerifiedHandler() { + @Override + public void onSuccess(RewardVerified rewardVerified) { + if (Lbryio.currentUser != null) { + Lbryio.currentUser.setRewardApproved(rewardVerified.isRewardApproved()); + } + + if (!rewardVerified.isRewardApproved()) { + // show pending purchase message (possible slow card tx) + Snackbar.make(findViewById(R.id.verification_pager), R.string.purchase_request_pending, Snackbar.LENGTH_LONG).show(); + } else { + Snackbar.make(findViewById(R.id.verification_pager), R.string.reward_verification_successful, Snackbar.LENGTH_LONG).show(); + } + + setResult(RESULT_OK); + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + finish(); + } + }, 3000); + } + + @Override + public void onError(Exception error) { + showFetchUserError(getString(R.string.purchase_request_failed_error)); + hideLoading(); + } + }); + } + } + } + } + }; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -73,6 +137,12 @@ public class VerificationActivity extends FragmentActivity implements SignInList }; registerReceiver(sdkReceiver, filter); + billingClient = BillingClient.newBuilder(this) + .setListener(purchasesUpdatedListener) + .enablePendingPurchases() + .build(); + establishBillingClientConnection(); + setContentView(R.layout.activity_verification); ViewPager2 viewPager = findViewById(R.id.verification_pager); viewPager.setUserInputEnabled(false); @@ -89,6 +159,24 @@ public class VerificationActivity extends FragmentActivity implements SignInList }); } + private void establishBillingClientConnection() { + if (billingClient != null) { + billingClient.startConnection(new BillingClientStateListener() { + @Override + public void onBillingSetupFinished(@NonNull BillingResult billingResult) { + if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { + // no need to do anything here. purchases are always checked server-side + } + } + + @Override + public void onBillingServiceDisconnected() { + establishBillingClientConnection(); + } + }); + } + } + public void onResume() { super.onResume(); LbryAnalytics.setCurrentScreen(this, "Verification", "Verification"); @@ -104,11 +192,13 @@ public class VerificationActivity extends FragmentActivity implements SignInList flowHandled = true; } else if (flow == VERIFICATION_FLOW_REWARDS) { User user = Lbryio.currentUser; - if (!user.isIdentityVerified()) { + // disable phone verification for now + /*if (!user.isIdentityVerified()) { // phone number verification required viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_PHONE, false); flowHandled = true; - } else if (!user.isRewardApproved()) { + } else */ + if (!user.isRewardApproved()) { // manual verification required viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL, false); flowHandled = true; @@ -195,10 +285,14 @@ public class VerificationActivity extends FragmentActivity implements SignInList ViewPager2 viewPager = findViewById(R.id.verification_pager); // for rewards, (show phone verification if not done, or manual verification if required) if (flow == VERIFICATION_FLOW_REWARDS) { - if (!user.isIdentityVerified()) { + // skipping phone verification + /*if (!user.isIdentityVerified()) { // phone number verification required viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_PHONE, false); - } else if (!user.isRewardApproved()) { + } else + */ + if (!user.isRewardApproved()) { + // manual verification required viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL, false); } else { @@ -289,6 +383,54 @@ public class VerificationActivity extends FragmentActivity implements SignInList findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE); } + @Override + public void onSkipQueueAction() { + if (billingClient != null) { + List skuList = new ArrayList<>(); + skuList.add(MainActivity.SKU_SKIP); + + SkuDetailsParams detailsParams = SkuDetailsParams.newBuilder(). + setType(BillingClient.SkuType.INAPP). + setSkusList(skuList).build(); + billingClient.querySkuDetailsAsync(detailsParams, new SkuDetailsResponseListener() { + @Override + public void onSkuDetailsResponse(@NonNull BillingResult billingResult, @Nullable List list) { + if (list != null && list.size() > 0) { + // we only queried one product, so it should be the first item in the list + SkuDetails skuDetails = list.get(0); + + // launch the billing flow for skip queue + BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder(). + setSkuDetails(skuDetails).build(); + billingClient.launchBillingFlow(VerificationActivity.this, billingFlowParams); + } + } + }); + } + } + + @Override + public void onTwitterVerified() { + Snackbar.make(findViewById(R.id.verification_pager), R.string.reward_verification_successful, Snackbar.LENGTH_LONG).show(); + + setResult(RESULT_OK); + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + finish(); + } + }, 3000); + } + + @Override + public void onManualProgress(boolean progress) { + if (progress) { + findViewById(R.id.verification_close_button).setVisibility(View.GONE); + } else { + findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE); + } + } + @Override public void onDestroy() { Helper.unregisterReceiver(sdkReceiver, this); diff --git a/app/src/main/java/io/lbry/browser/listener/SignInListener.java b/app/src/main/java/io/lbry/browser/listener/SignInListener.java index 866e2774..74d8ddf9 100644 --- a/app/src/main/java/io/lbry/browser/listener/SignInListener.java +++ b/app/src/main/java/io/lbry/browser/listener/SignInListener.java @@ -7,4 +7,7 @@ public interface SignInListener { void onPhoneAdded(String countryCode, String phoneNumber); void onPhoneVerified(); void onManualVerifyContinue(); + void onSkipQueueAction(); + void onTwitterVerified(); + void onManualProgress(boolean progress); } diff --git a/app/src/main/java/io/lbry/browser/model/TwitterOauth.java b/app/src/main/java/io/lbry/browser/model/TwitterOauth.java new file mode 100644 index 00000000..9d0bc847 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/model/TwitterOauth.java @@ -0,0 +1,9 @@ +package io.lbry.browser.model; + +import lombok.Data; + +@Data +public class TwitterOauth { + private String oauthToken; + private String oauthTokenSecret; +} diff --git a/app/src/main/java/io/lbry/browser/model/lbryinc/RewardVerified.java b/app/src/main/java/io/lbry/browser/model/lbryinc/RewardVerified.java new file mode 100644 index 00000000..88713110 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/model/lbryinc/RewardVerified.java @@ -0,0 +1,9 @@ +package io.lbry.browser.model.lbryinc; + +import lombok.Data; + +@Data +public class RewardVerified { + private long userId; + private boolean isRewardApproved; +} diff --git a/app/src/main/java/io/lbry/browser/tasks/RewardVerifiedHandler.java b/app/src/main/java/io/lbry/browser/tasks/RewardVerifiedHandler.java new file mode 100644 index 00000000..4c78fee7 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/RewardVerifiedHandler.java @@ -0,0 +1,8 @@ +package io.lbry.browser.tasks; + +import io.lbry.browser.model.lbryinc.RewardVerified; + +public interface RewardVerifiedHandler { + void onSuccess(RewardVerified rewardVerified); + void onError(Exception error); +} diff --git a/app/src/main/java/io/lbry/browser/tasks/TwitterOauthHandler.java b/app/src/main/java/io/lbry/browser/tasks/TwitterOauthHandler.java new file mode 100644 index 00000000..49357ced --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/TwitterOauthHandler.java @@ -0,0 +1,8 @@ +package io.lbry.browser.tasks; + +import io.lbry.browser.model.TwitterOauth; + +public interface TwitterOauthHandler { + void onSuccess(TwitterOauth twitterOauth); + void onError(Exception error); +} diff --git a/app/src/main/java/io/lbry/browser/tasks/lbryinc/AndroidPurchaseTask.java b/app/src/main/java/io/lbry/browser/tasks/lbryinc/AndroidPurchaseTask.java new file mode 100644 index 00000000..67a2ba8e --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/lbryinc/AndroidPurchaseTask.java @@ -0,0 +1,68 @@ +package io.lbry.browser.tasks.lbryinc; + +import android.content.Context; +import android.os.AsyncTask; +import android.view.View; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import org.json.JSONObject; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +import io.lbry.browser.MainActivity; +import io.lbry.browser.model.lbryinc.RewardVerified; +import io.lbry.browser.tasks.RewardVerifiedHandler; +import io.lbry.browser.utils.Helper; +import io.lbry.browser.utils.Lbryio; +import okhttp3.Response; + +public class AndroidPurchaseTask extends AsyncTask { + private Context context; + private View progressView; + private String purchaseToken; + private RewardVerifiedHandler handler; + private Exception error; + + public AndroidPurchaseTask(String purchaseToken, View progressView, Context context, RewardVerifiedHandler handler) { + this.purchaseToken = purchaseToken; + this.progressView = progressView; + this.context = context; + this.handler = handler; + } + + protected void onPreExecute() { + Helper.setViewVisibility(progressView, View.VISIBLE); + } + + protected RewardVerified doInBackground(Void... params) { + try { + Map options = new HashMap<>(); + options.put("purchase_token", purchaseToken); + + JSONObject object = (JSONObject) Lbryio.parseResponse(Lbryio.call("verification", "android_purchase", options, context)); + Type type = new TypeToken(){}.getType(); + Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + return gson.fromJson(object.toString(), type); + } catch (Exception ex) { + error = ex; + return null; + } + } + + protected void onPostExecute(RewardVerified result) { + Helper.setViewVisibility(progressView, View.GONE); + if (handler != null) { + if (result != null) { + handler.onSuccess(result); + } else { + handler.onError(error); + } + } + } +} diff --git a/app/src/main/java/io/lbry/browser/tasks/lbryinc/TwitterVerifyTask.java b/app/src/main/java/io/lbry/browser/tasks/lbryinc/TwitterVerifyTask.java new file mode 100644 index 00000000..265125e3 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/lbryinc/TwitterVerifyTask.java @@ -0,0 +1,68 @@ +package io.lbry.browser.tasks.lbryinc; + +import android.content.Context; +import android.os.AsyncTask; +import android.view.View; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import org.json.JSONObject; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +import io.lbry.browser.model.TwitterOauth; +import io.lbry.browser.model.lbryinc.RewardVerified; +import io.lbry.browser.tasks.RewardVerifiedHandler; +import io.lbry.browser.utils.Helper; +import io.lbry.browser.utils.Lbryio; + +public class TwitterVerifyTask extends AsyncTask { + private Context context; + private View progressView; + private TwitterOauth twitterOauth; + private RewardVerifiedHandler handler; + private Exception error; + + public TwitterVerifyTask(TwitterOauth twitterOauth, View progressView, Context context, RewardVerifiedHandler handler) { + this.twitterOauth = twitterOauth; + this.progressView = progressView; + this.context = context; + this.handler = handler; + } + + protected void onPreExecute() { + Helper.setViewVisibility(progressView, View.VISIBLE); + } + + protected RewardVerified doInBackground(Void... params) { + try { + Map options = new HashMap<>(); + options.put("oauth_token", twitterOauth.getOauthToken()); + options.put("oauth_token_secret", twitterOauth.getOauthTokenSecret()); + + JSONObject object = (JSONObject) Lbryio.parseResponse(Lbryio.call("verification", "twitter_verify", options, context)); + Type type = new TypeToken(){}.getType(); + Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + return gson.fromJson(object.toString(), type); + } catch (Exception ex) { + error = ex; + return null; + } + } + + protected void onPostExecute(RewardVerified result) { + Helper.setViewVisibility(progressView, View.GONE); + if (handler != null) { + if (result != null) { + handler.onSuccess(result); + } else { + handler.onError(error); + } + } + } +} diff --git a/app/src/main/java/io/lbry/browser/tasks/verification/TwitterAccessTokenTask.java b/app/src/main/java/io/lbry/browser/tasks/verification/TwitterAccessTokenTask.java new file mode 100644 index 00000000..c114d8f0 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/verification/TwitterAccessTokenTask.java @@ -0,0 +1,68 @@ +package io.lbry.browser.tasks.verification; + +import android.os.AsyncTask; + +import com.google.api.client.auth.oauth.OAuthHmacSigner; +import com.google.api.client.auth.oauth.OAuthParameters; +import com.google.api.client.http.GenericUrl; + +import io.lbry.browser.VerificationActivity; +import io.lbry.browser.model.TwitterOauth; +import io.lbry.browser.tasks.TwitterOauthHandler; +import io.lbry.browser.utils.Helper; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class TwitterAccessTokenTask extends AsyncTask { + private static final String ENDPOINT = "https://api.twitter.com/oauth/access_token"; + + private Exception error; + private String oauthParams; + private TwitterOauthHandler handler; + + public TwitterAccessTokenTask(String oauthParams, TwitterOauthHandler handler) { + this.oauthParams = oauthParams; + this.handler = handler; + } + + public String doInBackground(Void... params) { + try { + String url = String.format("%s?%s", ENDPOINT, oauthParams); + RequestBody body = RequestBody.create(new byte[0]); + Request request = new Request.Builder().url(url).post(body).build(); + + OkHttpClient client = new OkHttpClient.Builder().build(); + Response response = client.newCall(request).execute(); + return response.body().string(); + } catch (Exception ex) { + error = ex; + return null; + } + } + + protected void onPostExecute(String response) { + if (!Helper.isNullOrEmpty(response)) { + String[] pairs = response.split("&"); + TwitterOauth twitterOauth = new TwitterOauth(); + for (String pair : pairs) { + String[] parts = pair.split("="); + if (parts.length != 2) { + continue; + } + String key = parts[0]; + String value = parts[1]; + if ("oauth_token".equalsIgnoreCase(key)) { + twitterOauth.setOauthToken(value); + } else if ("oauth_token_secret".equalsIgnoreCase(key)) { + twitterOauth.setOauthTokenSecret(value); + } + } + handler.onSuccess(twitterOauth); + } else { + handler.onError(error); + } + } +} diff --git a/app/src/main/java/io/lbry/browser/tasks/verification/TwitterRequestTokenTask.java b/app/src/main/java/io/lbry/browser/tasks/verification/TwitterRequestTokenTask.java new file mode 100644 index 00000000..4082f80b --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/verification/TwitterRequestTokenTask.java @@ -0,0 +1,88 @@ +package io.lbry.browser.tasks.verification; + +import android.net.Uri; +import android.os.AsyncTask; +import android.util.Base64; + +import com.google.api.client.auth.oauth.OAuthHmacSigner; +import com.google.api.client.auth.oauth.OAuthParameters; +import com.google.api.client.http.GenericUrl; + +import java.nio.charset.StandardCharsets; + +import io.lbry.browser.VerificationActivity; +import io.lbry.browser.model.TwitterOauth; +import io.lbry.browser.tasks.TwitterOauthHandler; +import io.lbry.browser.utils.Helper; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class TwitterRequestTokenTask extends AsyncTask { + private static final String ENDPOINT = "https://api.twitter.com/oauth/request_token"; + + private String consumerKey; + private String consumerSecret; + private Exception error; + private TwitterOauthHandler handler; + + public TwitterRequestTokenTask(String consumerKey, String consumerSecret, TwitterOauthHandler handler) { + this.consumerKey = consumerKey; + this.consumerSecret = consumerSecret; + this.handler = handler; + } + + public String doInBackground(Void... params) { + try { + + OAuthHmacSigner signer = new OAuthHmacSigner(); + signer.clientSharedSecret = new String( + Base64.decode(consumerSecret, Base64.NO_WRAP), StandardCharsets.UTF_8.name()); + + OAuthParameters oauthParams = new OAuthParameters(); + oauthParams.callback = "https://lbry.tv"; + oauthParams.consumerKey = new String( + Base64.decode(consumerKey, Base64.NO_WRAP), StandardCharsets.UTF_8.name());; + oauthParams.signatureMethod = "HMAC-SHA-1"; + oauthParams.signer = signer; + oauthParams.computeNonce(); + oauthParams.computeTimestamp(); + oauthParams.computeSignature("POST", new GenericUrl(ENDPOINT)); + + RequestBody body = RequestBody.create(new byte[0]); + Request request = new Request.Builder().url(ENDPOINT).addHeader( + "Authorization", oauthParams.getAuthorizationHeader()).post(body).build(); + + OkHttpClient client = new OkHttpClient.Builder().build(); + Response response = client.newCall(request).execute(); + return response.body().string(); + } catch (Exception ex) { + error = ex; + return null; + } + } + + protected void onPostExecute(String response) { + if (!Helper.isNullOrEmpty(response)) { + String[] pairs = response.split("&"); + TwitterOauth twitterOauth = new TwitterOauth(); + for (String pair : pairs) { + String[] parts = pair.split("="); + if (parts.length != 2) { + continue; + } + String key = parts[0]; + String value = parts[1]; + if ("oauth_token".equalsIgnoreCase(key)) { + twitterOauth.setOauthToken(value); + } else if ("oauth_token_secret".equalsIgnoreCase(key)) { + twitterOauth.setOauthTokenSecret(value); + } + } + handler.onSuccess(twitterOauth); + } else { + handler.onError(error); + } + } +} diff --git a/app/src/main/java/io/lbry/browser/ui/verification/ManualVerificationFragment.java b/app/src/main/java/io/lbry/browser/ui/verification/ManualVerificationFragment.java index e901cb13..7bc09379 100644 --- a/app/src/main/java/io/lbry/browser/ui/verification/ManualVerificationFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/verification/ManualVerificationFragment.java @@ -1,27 +1,75 @@ package io.lbry.browser.ui.verification; +import android.content.Context; +import android.graphics.Color; +import android.os.AsyncTask; import android.os.Bundle; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.LinearLayout; +import android.widget.PopupWindow; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import com.google.android.material.button.MaterialButton; +import com.google.android.material.snackbar.Snackbar; + import io.lbry.browser.R; import io.lbry.browser.listener.SignInListener; +import io.lbry.browser.model.TwitterOauth; +import io.lbry.browser.model.lbryinc.RewardVerified; +import io.lbry.browser.tasks.RewardVerifiedHandler; +import io.lbry.browser.tasks.TwitterOauthHandler; +import io.lbry.browser.tasks.lbryinc.TwitterVerifyTask; +import io.lbry.browser.tasks.verification.TwitterAccessTokenTask; +import io.lbry.browser.tasks.verification.TwitterRequestTokenTask; import io.lbry.browser.utils.Helper; +import io.lbry.browser.utils.Lbryio; import lombok.Setter; public class ManualVerificationFragment extends Fragment { @Setter private SignInListener listener; + private PopupWindow popup; + private View mainView; + private View loadingView; + + private TwitterOauth currentOauth; + private boolean twitterOauthInProgress = false; + + private static final double SKIP_QUEUE_PRICE = 4.99; public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_verification_manual, container, false); - Helper.applyHtmlForTextView((TextView) root.findViewById(R.id.verification_manual_discord_verify)); + mainView = root.findViewById(R.id.verification_manual_main); + loadingView = root.findViewById(R.id.verification_manual_loading); + + Context context = getContext(); + MaterialButton buttonSkipQueue = root.findViewById(R.id.verification_manual_skip_queue); + buttonSkipQueue.setText(context.getString(R.string.skip_queue_button_text, String.valueOf(SKIP_QUEUE_PRICE))); + + Helper.applyHtmlForTextView(root.findViewById(R.id.verification_manual_discord_verify)); + root.findViewById(R.id.verification_manual_twitter_button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // start twitter verification + if (currentOauth != null) { + // Twitter three-legged oauth already completed, verify directly + twitterVerify(currentOauth); + } else { + // show twitter sign-in flow + twitterVerificationFlow(); + } + } + }); + + root.findViewById(R.id.verification_manual_continue_button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -31,6 +79,205 @@ public class ManualVerificationFragment extends Fragment { } }); + root.findViewById(R.id.verification_manual_skip_queue).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (listener != null) { + listener.onSkipQueueAction(); + } + } + }); + return root; } + + private void twitterVerificationFlow() { + twitterOauthInProgress = true; + if (listener != null) { + listener.onManualProgress(twitterOauthInProgress); + } + showLoading(); + String consumerKey = getResources().getString(R.string.TWITTER_CONSUMER_KEY); + String consumerSecret = getResources().getString(R.string.TWITTER_CONSUMER_SECRET); + TwitterRequestTokenTask task = new TwitterRequestTokenTask(consumerKey, consumerSecret, new TwitterOauthHandler() { + @Override + public void onSuccess(TwitterOauth twitterOauth) { + twitterOauthInProgress = false; + if (listener != null) { + listener.onManualProgress(twitterOauthInProgress); + } + showTwitterAuthenticateWithToken(twitterOauth.getOauthToken()); + } + + @Override + public void onError(Exception error) { + handleFlowError(null); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + public void showLoading() { + Helper.setViewVisibility(mainView, View.INVISIBLE); + Helper.setViewVisibility(loadingView, View.VISIBLE); + } + + public void hideLoading() { + Helper.setViewVisibility(mainView, View.VISIBLE); + Helper.setViewVisibility(loadingView, View.GONE); + } + + private void showTwitterAuthenticateWithToken(String oauthToken) { + Context context = getContext(); + if (context != null) { + WebView webView = new WebView(context); + webView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)); + webView.loadUrl(String.format("https://api.twitter.com/oauth/authorize?oauth_token=%s", oauthToken)); + webView.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (url.startsWith("https://lbry.tv") || url.equalsIgnoreCase("https://twitter.com/home") /* Return to Twitter */) { + if (url.startsWith("https://lbry.tv") && url.contains("oauth_token") && url.contains("oauth_verifier")) { + // finish 3-legged oauth + twitterOauthInProgress = true; + listener.onManualProgress(twitterOauthInProgress); + finishTwitterOauth(url); + } + + if (popup != null) { + popup.dismiss(); + } + return false; + } + + view.loadUrl(url); + return true; + } + }); + + View popupView = LayoutInflater.from(context).inflate(R.layout.popup_webview, null); + ((LinearLayout) popupView.findViewById(R.id.popup_webivew_container)).addView(webView); + popupView.findViewById(R.id.popup_cancel_button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (!twitterOauthInProgress && popup != null) { + popup.dismiss(); + hideLoading(); + } + } + }); + + float scale = getResources().getDisplayMetrics().density; + popup = new PopupWindow(context); + popup.setOnDismissListener(new PopupWindow.OnDismissListener() { + @Override + public void onDismiss() { + if (!twitterOauthInProgress) { + hideLoading(); + } + popup = null; + } + }); + popup.setWidth(Helper.getScaledValue(340, scale)); + popup.setHeight(Helper.getScaledValue(480, scale)); + popup.setContentView(popupView); + + View parent = getView(); + popup.setFocusable(true); + popup.showAtLocation(parent, Gravity.CENTER, 0, 0); + popup.update(); + } + } + + private void finishTwitterOauth(String callbackUrl) { + String params = callbackUrl.substring(callbackUrl.indexOf('?') + 1); + TwitterAccessTokenTask task = new TwitterAccessTokenTask(params, new TwitterOauthHandler() { + @Override + public void onSuccess(TwitterOauth twitterOauth) { + // send request to finish verifying + currentOauth = twitterOauth; + twitterVerify(twitterOauth); + } + + @Override + public void onError(Exception error) { + handleFlowError(null); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void twitterVerify(TwitterOauth twitterOauth) { + Context context = getContext(); + if (context != null) { + showLoading(); + twitterOauthInProgress = true; + if (listener != null) { + listener.onManualProgress(twitterOauthInProgress); + } + + TwitterVerifyTask task = new TwitterVerifyTask(twitterOauth, null, context, new RewardVerifiedHandler() { + @Override + public void onSuccess(RewardVerified rewardVerified) { + twitterOauthInProgress = false; + if (listener != null) { + listener.onManualProgress(twitterOauthInProgress); + } + + if (Lbryio.currentUser != null) { + Lbryio.currentUser.setRewardApproved(rewardVerified.isRewardApproved()); + } + if (rewardVerified.isRewardApproved()) { + if (listener != null) { + listener.onTwitterVerified(); + } + } else { + View root = getView(); + if (root != null) { + // reward approved wasn't set to true + Snackbar.make(root, getString(R.string.twitter_verification_not_approved), Snackbar.LENGTH_LONG). + setTextColor(Color.WHITE). + setBackgroundTint(Color.RED).show(); + } + hideLoading(); + } + } + + @Override + public void onError(Exception error) { + handleFlowError(error != null ? error.getMessage() : null); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + twitterOauthInProgress = false; + if (listener != null) { + listener.onManualProgress(twitterOauthInProgress); + } + hideLoading(); + } + } + + private void handleFlowError(String extra) { + hideLoading(); + twitterOauthInProgress = false; + if (listener != null) { + listener.onManualProgress(twitterOauthInProgress); + } + showFlowError(extra); + } + + private void showFlowError(String extra) { + Context context = getContext(); + View root = getView(); + if (context != null && root != null) { + String message = !Helper.isNullOrEmpty(extra) ? + getString(R.string.twitter_account_ineligible, extra) : + getString(R.string.twitter_verification_failed); + + Snackbar.make(root, message, Snackbar.LENGTH_LONG). + setTextColor(Color.WHITE). + setBackgroundTint(Color.RED).show(); + } + } } diff --git a/app/src/main/java/io/lbry/browser/utils/Lbry.java b/app/src/main/java/io/lbry/browser/utils/Lbry.java index 27c3ce38..1d0a7f5c 100644 --- a/app/src/main/java/io/lbry/browser/utils/Lbry.java +++ b/app/src/main/java/io/lbry/browser/utils/Lbry.java @@ -1,5 +1,7 @@ package io.lbry.browser.utils; +import android.util.Log; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -128,7 +130,7 @@ public final class Lbry { IS_STATUS_PARSED = true; } catch (JSONException | LbryResponseException ex) { // pass - android.util.Log.e(TAG, "Could not parse status response.", ex); + Log.e(TAG, "Could not parse status response.", ex); } } diff --git a/app/src/main/java/io/lbry/browser/utils/Lbryio.java b/app/src/main/java/io/lbry/browser/utils/Lbryio.java index 0b3561ac..4f157605 100644 --- a/app/src/main/java/io/lbry/browser/utils/Lbryio.java +++ b/app/src/main/java/io/lbry/browser/utils/Lbryio.java @@ -101,6 +101,7 @@ public final class Lbryio { } url = uriBuilder.build().toString(); } + /*if (BuildConfig.DEBUG) { Log.d(TAG, String.format("Request Method: %s, Sending request to URL: %s", method, url)); }*/ @@ -200,6 +201,7 @@ public final class Lbryio { throw new LbryioResponseException("Unknown API error signature.", response.code()); } } catch (JSONException | IOException ex) { + throw new LbryioResponseException(String.format("Could not parse response: %s", responseString), ex); } } @@ -230,7 +232,7 @@ public final class Lbryio { } } - android.util.Log.e(TAG, "Could not retrieve the current user", ex); + Log.e(TAG, "Could not retrieve the current user", ex); return null; } } @@ -308,7 +310,7 @@ public final class Lbryio { context.sendBroadcast(intent); } } catch (Exception ex) { - android.util.Log.e(TAG, "Error sending encrypted auth token action broadcast", ex); + Log.e(TAG, "Error sending encrypted auth token action broadcast", ex); // pass } } diff --git a/app/src/main/res/layout/activity_verification.xml b/app/src/main/res/layout/activity_verification.xml index 04fddc26..95ef31fd 100644 --- a/app/src/main/res/layout/activity_verification.xml +++ b/app/src/main/res/layout/activity_verification.xml @@ -11,6 +11,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + - - + + + android:layout_height="match_parent"> + + + - - - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popup_webview.xml b/app/src/main/res/layout/popup_webview.xml new file mode 100644 index 00000000..f58d2423 --- /dev/null +++ b/app/src/main/res/layout/popup_webview.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5b527f8..92016e96 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -422,8 +422,18 @@ Please enter your phone number. Not interested Manual Reward Verification + Reward Verification + Social Media Verification + Skip the Queue + Skip for $%1$s This account must undergo review before you can participate in the rewards program. This can take anywhere from several minutes to several days. - If you continue to see this message, please request to be verified on the <a href="https://discordapp.com/invite/Z3bERWA">LBRY Discord server</a>. + You can get instantly verified to be able to participate in the rewards program using your Twitter account or skipping the manual verification queue. + Twitter Verification + Get instantly verified using your Twitter account. Your Twitter email address must match the email that you provided and your account should be active. + Verify with Twitter + Skip the Queue + Skip the manual verification queue by paying a fee in order to start participating in the rewards program immediately. + Please request to be verified on the <a href="https://discordapp.com/invite/Z3bERWA">LBRY Discord server</a>. A manual review can take anywhere from several minutes to several days. Please enjoy free content in the meantime! Verify Phone Number Please enter the verification code sent to %1$s @@ -432,6 +442,12 @@ Please enter a valid phone number. Please enter the verification code sent to your phone number. User account could not be retrieved at this time. Please try again later. + Your purchase request is still being processed. Please send an email to support@lbry.com. + Your purchase request could not be completed at this time. Please send an email to support@lbry.com. + Your Twitter account is not eligible at this time: %1$s + Your account was not approved for the rewards program. Please try again later. + Twitter verification failed. Please try again later. + You are now eligible to participate in the rewards program! You have not added any tags yet. Add tags to improve discovery. diff --git a/app/twitter.properties.secret b/app/twitter.properties.secret new file mode 100644 index 00000000..9450afb8 Binary files /dev/null and b/app/twitter.properties.secret differ diff --git a/lbry-android.keystore.secret b/lbry-android.keystore.secret index 5ade2249..49c87412 100644 Binary files a/lbry-android.keystore.secret and b/lbry-android.keystore.secret differ