From 260d2c7ceaac64a9f0b7a16a1e058525b98be715 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 7 Mar 2023 15:26:37 -0500 Subject: [PATCH] Inverted flow happy path When doing first-time signup, create an account on the onboarding server and jump into register command at cheogram. When that command is done, we expect it will give us an OOB with xmmp:domain.tld?register which will jump us into magic create flow for that server. Once the new account is setup, if all we previously had was an onboarding account we execute jidSwitch and delete the onboarding account. If anything goes wrong you are left with the onboarding account still tied to cheogram and set up in the app as a "normal" account. If the registration finishes without jumping you to a ?register URI you are left with the onboarding account, but if you make an account then it will do the switch after that (it tries the switch blindly, without checking if the current backend supports it). If you press "no thanks" you just get a spinner forever. --- .../conversations/ui/WelcomeActivity.java | 42 ++++++++- .../java/eu/siacs/conversations/Config.java | 1 + .../conversations/entities/Conversation.java | 8 +- .../services/XmppConnectionService.java | 4 + .../ui/ConversationFragment.java | 3 + .../ui/ConversationsActivity.java | 4 +- .../ui/StartConversationActivity.java | 92 ++++++++++++++++++- 7 files changed, 141 insertions(+), 13 deletions(-) diff --git a/src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java index 9f91e0b687c8299ba15b8ab8e9573e0d74337e2c..5c23520c596c8381b7e8c934b33bbc3bc2725901 100644 --- a/src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java +++ b/src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java @@ -17,15 +17,18 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.databinding.DataBindingUtil; +import java.security.SecureRandom; import java.util.Arrays; import java.util.List; import java.util.HashSet; +import java.util.UUID; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityWelcomeBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.InstallReferrerUtils; import eu.siacs.conversations.utils.SignupUtils; @@ -35,11 +38,12 @@ import eu.siacs.conversations.xmpp.Jid; import static eu.siacs.conversations.utils.PermissionUtils.allGranted; import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; -public class WelcomeActivity extends XmppActivity implements XmppConnectionService.OnAccountCreated, KeyChainAliasCallback { +public class WelcomeActivity extends XmppActivity implements XmppConnectionService.OnAccountCreated, XmppConnectionService.OnAccountUpdate, KeyChainAliasCallback { private static final int REQUEST_IMPORT_BACKUP = 0x63fb; private XmppUri inviteUri; + private Account onboardingAccount = null; public static void launch(AppCompatActivity activity) { Intent intent = new Intent(activity, WelcomeActivity.class); @@ -82,8 +86,21 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi } @Override - protected void refreshUiReal() { + protected synchronized void refreshUiReal() { + if (onboardingAccount == null) return; + if (onboardingAccount.getStatus() != Account.State.ONLINE) return; + Intent intent = new Intent(this, StartConversationActivity.class); + intent.putExtra("init", true); + intent.putExtra(EXTRA_ACCOUNT, onboardingAccount.getJid().asBareJid().toEscapedString()); + onboardingAccount = null; + startActivity(intent); + finish(); + } + + @Override + public void onAccountUpdate() { + refreshUi(); } @Override @@ -124,9 +141,18 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi setSupportActionBar(binding.toolbar); configureActionBar(getSupportActionBar(), false); binding.registerNewAccount.setOnClickListener(v -> { - final Intent intent = new Intent(this, MagicCreateActivity.class); - addInviteUri(intent); - startActivity(intent); + if (hasInviteUri()) { + final Intent intent = new Intent(this, MagicCreateActivity.class); + addInviteUri(intent); + startActivity(intent); + } else { + binding.registerNewAccount.setText("Working..."); + binding.registerNewAccount.setEnabled(false); + onboardingAccount = new Account(Jid.ofLocalAndDomain(UUID.randomUUID().toString(), Config.ONBOARDING_DOMAIN.toEscapedString()), CryptoHelper.createPassword(new SecureRandom())); + onboardingAccount.setOption(Account.OPTION_REGISTER, true); + onboardingAccount.setOption(Account.OPTION_FIXED_USERNAME, true); + xmppConnectionService.createAccount(onboardingAccount); + } }); binding.useExisting.setOnClickListener(v -> { final List accounts = xmppConnectionService.getAccounts(); @@ -235,6 +261,12 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi } } + protected boolean hasInviteUri() { + final Intent from = getIntent(); + if (from != null && from.hasExtra(StartConversationActivity.EXTRA_INVITE_URI)) return true; + return this.inviteUri != null; + } + public void addInviteUri(Intent to) { final Intent from = getIntent(); if (from != null && from.hasExtra(StartConversationActivity.EXTRA_INVITE_URI)) { diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 681fdee3c7a3c9822aa3565d6c92b1a9f7215c66..73453583c237a0ad8b8c3e7b03fcb7a80f74c12d 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -48,6 +48,7 @@ public final class Config { public static final String DOMAIN_LOCK = null; //only allow account creation for this domain public static final String MAGIC_CREATE_DOMAIN = "chatterboxtown.us"; public static final Jid QUICKSY_DOMAIN = Jid.of("cheogram.com"); + public static final Jid ONBOARDING_DOMAIN = Jid.of("onboarding.cheogram.com"); public static final String CHANNEL_DISCOVERY = "https://search.jabber.network"; diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index c7ace79ffcc4c6b1e924022f743c2df500b93341..b09ff01fe7775abd573379780e61877efea918a3 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -797,7 +797,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } } } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) { - return contactJid; + return contactJid.equals(Jid.of("cheogram.com")) ? "Cheogram" : contactJid; } else { return this.getContact().getDisplayName(); } @@ -2400,7 +2400,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } public void updateWithResponse(final IqPacket iq) { - if (getView().isAttachedToWindow()) { + if (getView() != null && getView().isAttachedToWindow()) { getView().post(() -> updateWithResponseUiThread(iq)); } else { pendingResponsePacket = iq; @@ -2706,6 +2706,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } public View getView() { + if (mBinding == null) return null; return mBinding.getRoot(); } @@ -2874,8 +2875,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl actionsAdapter.notifyDataSetChanged(); if (pendingResponsePacket != null) { - updateWithResponseUiThread(pendingResponsePacket); + final IqPacket pending = pendingResponsePacket; pendingResponsePacket = null; + updateWithResponseUiThread(pending); } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index c4e7e4f6fb8daab7af161d951843f7311f5f8bd1..5f802cd165771568a8832f89e90d4c4282a9061f 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1805,6 +1805,10 @@ public class XmppConnectionService extends Service { sendMessage(message, true, delay); } + public boolean isOnboarding() { + return getAccounts().size() == 1 && getAccounts().get(0).getJid().getDomain().equals(Config.ONBOARDING_DOMAIN); + } + public void requestEasyOnboardingInvite(final Account account, final EasyOnboardingInvite.OnInviteRequested callback) { final XmppConnection connection = account.getXmppConnection(); final Jid jid = connection == null ? null : connection.getJidForCommand(Namespace.EASY_ONBOARDING_INVITE); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 264cca2bc2249b4d3c2491a9040e5020afa87f94..9ca8b274a04d0cfa486725d50765948ef1f71ea1 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1202,6 +1202,8 @@ public class ConversationFragment extends XmppFragment @Override public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { + if (activity.xmppConnectionService.isOnboarding()) return; + menuInflater.inflate(R.menu.fragment_conversation, menu); final MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details); final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details); @@ -2997,6 +2999,7 @@ public class ConversationFragment extends XmppFragment } if (node != null && commandJid != null) { conversation.startCommand(commandFor(commandJid, node), activity.xmppConnectionService); + if (activity.xmppConnectionService.isOnboarding()) binding.tabLayout.setVisibility(View.GONE); } }); return; diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index 5af53409fb5fbfba05b4e142f59fb3c9a92ef2f4..9c9a1e9a0c5f420f24920e015e7ce2b775faa84a 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -740,10 +740,10 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio final Conversation conversation = ((ConversationFragment) mainFragment).getConversation(); if (conversation != null) { actionBar.setTitle(conversation.getName()); - actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(!xmppConnectionService.isOnboarding()); ActionBarUtil.setActionBarOnClickListener( binding.toolbar, - (v) -> openConversationDetails(conversation) + (v) -> { if(!xmppConnectionService.isOnboarding()) openConversationDetails(conversation); } ); return; } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 6c6a9b0ea46e88147ef1853025ccb33903e2d214..139b80b1c85cb63f68e2ab5e6345623859120618 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -95,9 +95,13 @@ import eu.siacs.conversations.ui.widget.SwipeRefreshListFragment; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.XmppUri; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.forms.Data; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreatePrivateGroupChatDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener, CreatePublicChannelDialog.CreatePublicChannelDialogListener { @@ -918,8 +922,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne final String accountJid = intent.getStringExtra(EXTRA_ACCOUNT); intent = null; boolean hasPstnOrSms = false; + Account onboardingAccount = null; outer: for (Account account : xmppConnectionService.getAccounts()) { + if (onboardingAccount == null && account.getJid().getDomain().equals(Config.ONBOARDING_DOMAIN)) onboardingAccount = account; + if (accountJid != null) { if(account.getJid().asBareJid().toEscapedString().equals(accountJid)) { selectedAccount = account; @@ -941,9 +948,88 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } if (!hasPstnOrSms) { - startCommand(selectedAccount, Jid.of("cheogram.com/CHEOGRAM%jabber:iq:register"), "jabber:iq:register"); - finish(); - return; + if (onboardingAccount != null && !selectedAccount.getJid().equals(onboardingAccount.getJid())) { + final Account onboardAccount = onboardingAccount; + final Account newAccount = selectedAccount; + final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + packet.setTo(Jid.of("cheogram.com")); + final Element c = packet.addChild("command", Namespace.COMMANDS); + c.setAttribute("node", "change jabber id"); + c.setAttribute("action", "execute"); + + xmppConnectionService.sendIqPacket(onboardingAccount, packet, (a, iq) -> { + Element command = iq.findChild("command", "http://jabber.org/protocol/commands"); + if (command == null) { + Log.e(Config.LOGTAG, "Did not get expected data form from cheogram, got: " + iq); + return; + } + + Element form = command.findChild("x", "jabber:x:data"); + Data dataForm = form == null ? null : Data.parse(form); + if (dataForm == null || dataForm.getFieldByName("new-jid") == null) { + Log.e(Config.LOGTAG, "Did not get expected data form from cheogram, got: " + iq); + return; + } + + dataForm.put("new-jid", newAccount.getJid().toEscapedString()); + dataForm.submit(); + command.setAttribute("action", "execute"); + iq.setTo(iq.getFrom()); + iq.setAttribute("type", "set"); + iq.removeAttribute("from"); + iq.removeAttribute("id"); + xmppConnectionService.sendIqPacket(a, iq, (a2, iq2) -> { + Element command2 = iq2.findChild("command", "http://jabber.org/protocol/commands"); + if (command2 != null && command2.getAttribute("status") != null && command2.getAttribute("status").equals("completed")) { + final IqPacket regPacket = new IqPacket(IqPacket.TYPE.SET); + regPacket.setTo(Jid.of("cheogram.com/CHEOGRAM%jabber:iq:register")); + final Element c2 = regPacket.addChild("command", Namespace.COMMANDS); + c2.setAttribute("node", "jabber:iq:register"); + c2.setAttribute("action", "execute"); + xmppConnectionService.sendIqPacket(newAccount, regPacket, (a3, iq3) -> { + Element command3 = iq3.findChild("command", "http://jabber.org/protocol/commands"); + if (command3 == null) { + Log.e(Config.LOGTAG, "Did not get expected data form from cheogram, got: " + iq3); + return; + } + + Element form3 = command3.findChild("x", "jabber:x:data"); + Data dataForm3 = form3 == null ? null : Data.parse(form3); + if (dataForm3 == null || dataForm3.getFieldByName("confirm") == null) { + Log.e(Config.LOGTAG, "Did not get expected data form from cheogram, got: " + iq3); + return; + } + + dataForm3.put("confirm", "true"); + dataForm3.submit(); + command3.setAttribute("action", "execute"); + iq3.setTo(iq3.getFrom()); + iq3.setAttribute("type", "set"); + iq3.removeAttribute("from"); + iq3.removeAttribute("id"); + xmppConnectionService.sendIqPacket(newAccount, iq3, (a4, iq4) -> { + Element command4 = iq2.findChild("command", "http://jabber.org/protocol/commands"); + if (command4 != null && command4.getAttribute("status") != null && command4.getAttribute("status").equals("completed")) { + xmppConnectionService.createContact(newAccount.getRoster().getContact(iq4.getFrom().asBareJid()), true); + Conversation withCheogram = xmppConnectionService.findOrCreateConversation(newAccount, iq4.getFrom().asBareJid(), true, true, true); + xmppConnectionService.markRead(withCheogram); + xmppConnectionService.clearConversationHistory(withCheogram); + xmppConnectionService.deleteAccount(onboardAccount); + } else { + Log.e(Config.LOGTAG, "Error confirming jid switch, got: " + iq4); + } + }); + }); + } else { + Log.e(Config.LOGTAG, "Error during jid switch, got: " + iq2); + } + }); + }); + } else { + startCommand(selectedAccount, Jid.of("cheogram.com/CHEOGRAM%jabber:iq:register"), "jabber:iq:register"); + finish(); + return; + } } }