From 6931dbe79ca867f14a2cb1eb2af9ad14983f7ad8 Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Wed, 19 Aug 2020 17:23:35 +0100 Subject: [PATCH] Instant verification (#974) * add instant verification options and Google Play Billing bumpversion 0.15.16 --> 0.15.17 restore account_undergo_review default string twitter sign-in flow fix build error final changes update api key and secret * tweak build script --- .gitignore | 1 + .gitlab-ci.yml | 10 +- .gitsecret/keys/pubring.kbx | Bin 4929 -> 2480 bytes .gitsecret/keys/pubring.kbx~ | Bin 2481 -> 4929 bytes .gitsecret/keys/random_seed | Bin 600 -> 600 bytes .gitsecret/paths/mapping.cfg | 1 + app/build.gradle | 27 ++ app/google-services.json.secret | Bin 1028 -> 1025 bytes app/src/main/AndroidManifest.xml | 1 + .../java/io/lbry/browser/MainActivity.java | 110 +++++++- .../io/lbry/browser/VerificationActivity.java | 150 ++++++++++- .../lbry/browser/listener/SignInListener.java | 3 + .../io/lbry/browser/model/TwitterOauth.java | 9 + .../browser/model/lbryinc/RewardVerified.java | 9 + .../browser/tasks/RewardVerifiedHandler.java | 8 + .../browser/tasks/TwitterOauthHandler.java | 8 + .../tasks/lbryinc/AndroidPurchaseTask.java | 68 +++++ .../tasks/lbryinc/TwitterVerifyTask.java | 68 +++++ .../verification/TwitterAccessTokenTask.java | 68 +++++ .../verification/TwitterRequestTokenTask.java | 88 ++++++ .../ManualVerificationFragment.java | 251 +++++++++++++++++- .../main/java/io/lbry/browser/utils/Lbry.java | 4 +- .../java/io/lbry/browser/utils/Lbryio.java | 6 +- .../main/res/layout/activity_verification.xml | 9 +- .../layout/fragment_verification_manual.xml | 177 ++++++++---- app/src/main/res/layout/popup_webview.xml | 20 ++ app/src/main/res/values/strings.xml | 18 +- app/twitter.properties.secret | Bin 0 -> 731 bytes lbry-android.keystore.secret | Bin 2737 -> 2736 bytes 29 files changed, 1042 insertions(+), 72 deletions(-) create mode 100644 app/src/main/java/io/lbry/browser/model/TwitterOauth.java create mode 100644 app/src/main/java/io/lbry/browser/model/lbryinc/RewardVerified.java create mode 100644 app/src/main/java/io/lbry/browser/tasks/RewardVerifiedHandler.java create mode 100644 app/src/main/java/io/lbry/browser/tasks/TwitterOauthHandler.java create mode 100644 app/src/main/java/io/lbry/browser/tasks/lbryinc/AndroidPurchaseTask.java create mode 100644 app/src/main/java/io/lbry/browser/tasks/lbryinc/TwitterVerifyTask.java create mode 100644 app/src/main/java/io/lbry/browser/tasks/verification/TwitterAccessTokenTask.java create mode 100644 app/src/main/java/io/lbry/browser/tasks/verification/TwitterRequestTokenTask.java create mode 100644 app/src/main/res/layout/popup_webview.xml create mode 100644 app/twitter.properties.secret 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 b5b2e86856352ae291384524053c4d4404d21e6b..e16cdd1bb303bf2bc1454dbb71d55b9da3c267ad 100644 GIT binary patch delta 24 acmX@8wn2D;2v5ANZ6^a1Y~IK@ffE2wlm){8 delta 2387 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 delta 12 TcmX@8wo!P33M11-)gDd&94`aT diff --git a/.gitsecret/keys/random_seed b/.gitsecret/keys/random_seed index 86bb2f488618254ed86442080ed9f8b1c1b920c9..3708c62992ebc68d00875fc6d2618ac0354890c4 100644 GIT binary patch literal 600 zcmV-e0;l~F1{C3nV6qB}Wzi(_G9?z*d|RTjB9v?P_f5F$-@v_)Ga&>-3%4gH`es|h z`z#HIT;8-BavXZ|d6&OY@37xUFkp0cO@bDKBKlWq1Ibj_pGEUTNp*dusBL>_g=6Wz zXy{@Dh0)hHbOy#LS&XqD%NiEF4$|Kg=oH8Wr1t+TS&^wwV?T@r5aEgSq>8ozuUiPJ`&I1TG3>aR* zssX{iCQjERZ1&g`8YWAZ=16c?_}gsG|U$MyPzAt zqk`a3PmJ+eEpEKC$^&{b%$5IU^w>g1%)JaZ>RYlw&9w~~#1a~_=$HvhGGw7>GU;0F z%hpC<_6L0i*eY_J`8y}tE0SU{3Y-q|@LJ=?AE^MH zv!~pAA-u6FeR|S2Lf;cbPYtxK!>d^fr|Wdhm)`?zjpqve6xPzu355vh?$3GcVmxx= m=D_!e?S5=65-lofpGLL^=AV-5ykt45NqJNA`)E})L#d%zEiNzs literal 600 zcmV-e0;l~Jl+A3~-`ZUIex(sM`2z&No_t$#jU519#^2v;3lnOJUEQDMwl)WFz?BBH z$t0_ncbevBexKLzvPZ@xq}0d-w^%Snx-VcxrA2x9R+pj`&0&HR)sK%eN{G23nXjAq z`Og}J^zt~nT9~)_y+c5onULorkP-0m1rFuA&wIN}LOsEI+G?z0W7xBDt2m0Ek^i}9 zo^4sN6{p0S@M{k-uIlM0_QOh6XC5;M0gdLE!YY= z)e_0<582oV)22`VO^B?otB*5p8KTd^Fe_+`e*6z)soVJr0&<;})E8+qv-HB5EGT&L z6+GYm1!G6DoP|sedk_@@uQtQE9lW1iG~`OL7C1HMT(#2}wD@A$IXVMuNr07hd~TKt zgly4ei6%Pw>qq#CghGuyO1mwy&t}^cL>=TO3O>yyX%lDrTm$3Fw3xY>cq272fQ*Q{ znh&Z4^1y=K$<0UKrQ{dKYPxE9P%&YH2zopIz*bOS&fBP+ZiQzg*hMvjLYj!s0an{8 zU=byU=qb0xw~9HBE2JuyOypX=4L%5CSG!ItWb^Pmm*D4C4!8}akDGk!f<=rFs70G# z(6aK#Hvy*c?^Y4c7fX+?E{VFgAnhh*Hq5T?fr4l47vhrwzNR);&wcqq@g+VvC>)D$YD^xQ m7v*>iLVnmgX%modvuv<|MC|xq0*ax;fW+BTP+e|bYO~*yUMSuG 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 1e09a2e11a60a5cbc05f5f221726d79ceece29af..ada7f6f65d6cd275af2940f587399b5ebd745a40 100644 GIT binary patch literal 1025 zcmV+c1pfPl0t^G$jRG(+f;S!k5CE26H4MigDB>mE)9B0Q*b*b!jr{a4K9fItu0 zmVK$hD6Q8EkAy_Xsq<7#$Sv_?;8+Rc;SiwFl*zPbr0k=wlW4Czr)ub2j$n(TSzyEU zEtyPeB_&K8dx<@H32*m4iMUPuk;oXAdi@-CY+WdXS`(VAn9ct<{uiQF!*ta9-d~wr ztt923mHIKC)bKjLzl02T00Mnjc1mh%xVb$fAiH3aY%jtRJ_p66%9ShKQRkbTztlS( zcR@87xhv3+c%Qsn>&4utz%cqo6$zmy?%5y#RoKRC4xcw9A3Rm$rmM_-ur z`FO5;BOkEpoZXHS`Gz6RD8mbhrK~E6cvij?Ub8&U>v|#VoJ}DUn6d^VZ7w>9-o9L+ zVzY`3NQOhv_va5gszSKkL^Pw|=Ih*` zr~X+OS)}VU)ZQfLu9eckF9A-*yv%Z3@-$eU1xBh$N{?z(Y4_W?ESaWS8n)G)>l0Qq zm!fZjE4MtTl5IOS6+xW`;~j0cWRio3bM-CAjghsAqH~S^UyFGz0274~OnAn2+#*v$ zxVZEFm73#t@F8#@U5V83xBI| z$4VE$c&XsqYY;WaV6;D#^g*3P6+J=dY7EKo(@`yliFgk6L|KiQg@a*leUyPDYibki zlynFt)*nvJfXzqyl|Rh%RBkZTjpP&`;bVDtyiE5x3veuqd)}EMbqtq*6q_MASP6JR z;*U(ty@4^By|FHqQ>Bw-b1>s;>6qTN5+7h-3w_^$k9?lrdpRu=M_!OyujOtx;5>Uu z#AEo2iCX!?S7fFp0hrJ{u;4n)+?W1VeSir?gfQ3q`f-mwG v8D0y)-_)h)(8;`}kC1i4d?!Ut8iAENXUYo_so!a|pST-kotiOXy0HQ?2SN#R literal 1028 zcmV+f1pE7i0t^G$jRG(+f;S!k5CDP_h#=t?F)i&ddJ`J;R33=!!)f9t>gARzU~M7^ z2eqY`GEprb(X27|PS2luXP}>gkk4RLrZ*(OY>|H7rFm!P;ShOOtRAZsDq{7fG`Y|s4(AhU=CFd{U|rf*!cbqs-J6=y87|8Lj!F`rG5}vi zntY=K$r={L-F)Dg5b=O&AbqP_J91<-8_5&aXFziPw3GNId?1-C^_h~3!V3FD-h40) zTf=RoF1f*jocF8ZFuEktNJZFVy7T}f&Y4mRHSwkfw7qmE0SUWzGbv1b>$gh&oc#h4 zd3N=_L}xn4%zm_EM)3Gv68&DV5Mg{?;9fFjui79&-AGh6UmUHPE2AIt{tlaO0%O@` zB(ggp83xY3ZTW*c!<*Pfg4+rmvYJmJ2T*ud9wC6FEmZ6${YRW2>muLdn&-{Mw6so8 z*TB$-Pk{HzwLosK+v)3~cD-!(&YujhH%K>R3qijxPej3gl%Jx_hc=$oub*^}2!#y- zYiqIUW)1s47@W+SF_F^2G67Y;T}iyiGhk}$;XSg3+jnnG2d`z|{+O2U+IOXcfBAYE zb8Nz_Zq6kFT2rOW{*$#SAo=s?_0fFCHw<6MuS^LY#yyZl0cMev~XdB{>9i+vAtU?@rYEgAy$%d zp6l+Nih3jdfV2Gjtpb~ 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 0000000000000000000000000000000000000000..9450afb8953c540b08decf91016480a265fede33 GIT binary patch literal 731 zcmV<10wn!~0t^G$jRG(+f;S!k5B@ANgF~Fc?Xc!;te}Q1-co`-8iP9QEEABC7Des` zLNFHh%mzPcLXL$Nn*6xFd?r$=zGO>an!9>ADQEaKKa8Yd7U02bG8DmYwN8v&P2C{2 zSQHN;!%!|ie~cV)e;>WvL>W#WL^4flSOSB1l13=ca4$ZREpKL_dJCD*Zg1PME6@~S zb@-?jJ~o8R2nZ!jTtDr3QYLE8B&LS7TtNMlYq8UTCfkaD9~1!DSVBp!*FS&4 zBPPe~_^k&=6~Om6<$&3O!UJ|)U6tUQaGdEp|aZqf`>>8F67tEy7b0R|t_?Q}Y8 zPYtm!vK_z^3hQttoIgl5lwu3qoYEHM!VGeIG#FpcQjk~rmDP%-1>QhvHa-HwRefIA z^xV`4pB08U-l0PFot+yM&6&*Cl|tr~5P5)qU%Q8LrMD(zejEFx$SA(q2ZJjLLc7c~ zNp-u32zNGkxx{^HCT5ToUnA4|>I5Fzg{K=aNaiDe#;=0c_9fs1{rc6KOt5oqkb>=+ zcPd!#%Y)wgN84AO$(578&v*%Kmp6OE@id!7H%?s}K6PsoCkt6qPxSxk?r+IP=! z?Gz8tYxJo;3NSEYJJuPaV!sDi!u0)*1Q2>}dl7qCxF|0_@`jDY5m?t?12M~RcqZ-CY$p-(u=L~gBS zUfmTrhK{%1d;h*6FmBF0b9F?jWxDi=32i!Mb+&zzZbG3p;|y!vvjXR_na+TS_o+B4 zZuHPKgdGpLlhkq_vXdwcJ-BB>BV4o=;MnrPEfHbpe{u@#! zSKPu}38JKod2m_64vIjArICb{nUwUn#{pfwd$HDb+MsF|&ipix@(6Dtb$K$=mG7$D NP{btr7Au75CjVq^T>=0A literal 0 HcmV?d00001 diff --git a/lbry-android.keystore.secret b/lbry-android.keystore.secret index 5ade22492f0606c1dd67162ef8dcb3ee95070ccd..49c874123ede0572270d2dfe0ec1308a3fbd36d5 100644 GIT binary patch literal 2736 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 literal 2737 zcmV;i3QqNf0t^G$jRG(+f;S!k5CEr5(1zw*;YpoVs&ZVEVl~J9NM<|Hu;-RUt1gv}2zAr2 zOOjHC%ZWhMbJg{5EFLm18J8bqN;Y30k4*D+7#Q=wAhN8A-BCEz+LTJDL#t)TZtx)3(L zRZcpM8X2qHZk#vqF0$9>s{Twv8{gVxllmB_nyUoGzr2e!!nZ(A_g+VVD47$>xVxrh ziiLGE=`2PA34Nm*?LQ;hF=0q?$X)SbKs}_e50`zCF%a`g9l>$4!vD3lG$PAhIG<;R zejRs5{U3&*Y-0C8Iwk=GQ`}k={--t+^?17g90jHtRd0msm zNMj9V_QiPWi5uR1Bz}h%DY6uim^gk05N0oGyU>q*y8TsIaw=Y6W^jnf>#lfkxiZ0k zo`b-QS$RZQcNuRldi%R{_hGH-j$|DzYO2Pl4=0%wYTE3&ZZfWb zpL6j08nA|E;gr|EzDt)$F0kzQN^d8T?{@?w%{AuMsCX&g%f{hYtCb!-)LABdf$J|=Tb_Z<5r=Pl+$J<}aJl|)WJcpLtCkO_n$PMtkCRTAEbiBdGB~%zGqY5U- zmovJWhG1<;5HlgMa1v3g9Y=rg+vwDe-5ymym0r#?g5OdERY(&J510p;&Mg(dhtJx_ zRi(LX33sX2`ppn9X$`UAhI=k-;`bXRc3MdHcsaFDlkyz`i<_-#wzPrPggd0v){7Zf zm0HGNde{0$E%uJK>twO>nOAv#3~`bBl`VT`KyMCC$)iBba0r1a!Pv{{Owaf!ie%?3 zB9jre4~!NJut{IyMD#n$7tu$BieW1YPH7VOeoiA<#O!C=+x(fOp+}AqXy6eHr&+%G zn}xm4-Y}k1zep#wTK8jBuOgU~M}!7N19M?cq%kH?;XXkOg%5yyri@JWx=!7!0Y5{d z@8Wts)eE!Cv;%u~gz*aGV;5Qa`#WT?Awuo@IxW9UmvzD96{2xz(Ihv*RlgK19h~4A zr7Rc}l)OlH_darPwc{i~_0dx`sQmYLntqHAK_u2DBe6t~c--;DWCwvJt@IS&GW(FRK zlQ(VE(s5;_wz)BxZ=pQ|W{BjVHFG7U;1~-SVG+pXZhf`vA^xXk$+~-o7@FNA&?)CW z46mg9-SGeLUHkxL4`CSNam{tRW07o5?yzz8&5+v6Yw=E#>l1Dk3G`0X-5z^bc zV5eky#(j_#10-@5iL_tF^eSIbp;U~T5mvf@^r{v=VmM}Pe|1P!5P@9Y+WsF(L^r#y%h<2{*W)zEiAzT z&b8^6^NwBFZ1-fNN-&9~jdFjtDE9L+*Xn37EYSlT+NLc+WkKFCF8p&GCmu8mx`0^% zkG|zLq?B$-RNDyo(s+n_Lp07s$Vd>Gxzt=+R?!ztyyM)eN%=8tB1&*cOs>5B9=tfu zBW+~#wawAlrYSS`?#O%RWh`u%3Tx+g7nauq8Q8-({9uVMO?#MGovj2zA)|WK>d^dh|zw>Z#lHP z_MNKDtj$JZp%dow@NC7o;VBO#9#t0vQZ^*TWO8=CiuL1K$T#QvM%?sD=pP*t%v>&C z`7!faag|EWsr)%)q+uXAPuU+hjE47eVv zIT~YNG@C%IS%g>x^}3SsNhrrn939?o(7$y5q(>ZHM}5|x7CZwK8N$W0A)I&|lN^AU z@^z``H=_`|dIrwiFCwbw!XVld4cznoj~{?l+Dh4pE|rK|&Y|oNA(huR4O+eNG!}Qo zH`{@6?=L+zA7O5ugmFFl7k=93A?T%0FK7F?JC3A+5B6FqpV1HmXD|A<^lWHr9mM)B zhUY1qF3jw2 z0#-hm;-kjm-4sxyMpd99-VDo&gzfi5rz*H{#k8O+`xl^l%wN16FnT!>-y+8gDVvsn rSzl^2ueGtWQgein)~qMIIR~}Ieh5)Ydep5_fg41UPw>x~T>we)t=3Xp