Skip to content
This repository has been archived by the owner on Jul 12, 2023. It is now read-only.

Use Android intent links in place of bouncing. #845

Merged
merged 1 commit into from
Oct 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions pkg/controller/redirect/appstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,23 @@ import (
)

type AppStoreData struct {
AndroidURL string `json:"androidURL"`
IOSURL string `json:"iosURL"`
AndroidURL string `json:"androidURL"`
AndroidAppID string `json:"androidAppID"`
IOSURL string `json:"iosURL"`
}

// getAppStoreData finds data tied to app store listings.
func (c *Controller) getAppStoreData(realmID uint) (*AppStoreData, error) {
// Pick first Android app (in the realm) for Play Store redirect.
androidURL := ""
androidAppID := ""
androidApps, err := c.db.ListActiveAppsByOS(realmID, database.OSTypeAndroid)
if err != nil {
return nil, fmt.Errorf("failed to get Android Apps: %w", err)
}
if len(androidApps) > 0 {
androidURL = androidApps[0].URL
androidAppID = androidApps[0].AppID
}

// Pick first iOS app (in the realm) for Store redirect.
Expand All @@ -48,7 +51,8 @@ func (c *Controller) getAppStoreData(realmID uint) (*AppStoreData, error) {
}

return &AppStoreData{
AndroidURL: androidURL,
IOSURL: iosURL,
AndroidURL: androidURL,
AndroidAppID: androidAppID,
IOSURL: iosURL,
}, nil
}
41 changes: 25 additions & 16 deletions pkg/controller/redirect/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,23 +102,10 @@ func decideRedirect(region, userAgent string, url url.URL,
onAndroid := isAndroid(userAgent)
onIOS := isIOS(userAgent)

// A subset of SMS clients (e.g. Facebook Messenger) open links
// in inline WebViews without giving https Intents a opportunity
// to trigger App Links.
// Redirect to ourselves once to attempt to trigger the link.
// Bounce to self if we haven't already (on Android only).
// Keep track of state by including an extra bounce=1 url param.
if onAndroid && url.Query().Get("bounce") == "" {
q := url.Query()
q.Set("bounce", "1")
url.RawQuery = q.Encode()
return url.String(), true
}

// On Android redirect to Play Store if App Link doesn't trigger
// and an a link is set up.
if onAndroid && appStoreData.AndroidURL != "" {
return appStoreData.AndroidURL, true
if onAndroid && appStoreData.AndroidURL != "" && appStoreData.AndroidAppID != "" {
return buildIntentURL(path, url.Query(), region, appStoreData.AndroidAppID, appStoreData.AndroidURL), true
}

// On iOS redirect to App Store if App Link doesn't trigger
Expand All @@ -143,8 +130,30 @@ func buildEnsURL(path string, query url.Values, region string) string {
u.RawQuery = query.Encode()
q := u.Query()
q.Set("r", region)
q.Del("bounce")
u.RawQuery = q.Encode()

return u.String()
}

// buildIntentURL returns the ens:// URL with fallback
// for the given path, query, and region.
func buildIntentURL(path string, query url.Values, region, appID, fallback string) string {
u := &url.URL{
Scheme: "intent",
Path: strings.TrimPrefix(path, "/"),
}
u.RawQuery = query.Encode()
q := u.Query()
q.Set("r", region)
u.RawQuery = q.Encode()

suffix := "#Intent"
suffix += ";scheme=ens"
suffix += ";package=" + appID
suffix += ";action=android.intent.action.VIEW"
suffix += ";category=android.intent.category.BROWSABLE"
suffix += ";S.browser_fallback_url=" + url.QueryEscape(fallback)
suffix += ";end"

return u.String() + suffix
}
135 changes: 106 additions & 29 deletions pkg/controller/redirect/index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ func TestBuildEnsURL(t *testing.T) {
exp: "ens://v?r=US-AA",
},
{
name: "drop_bounce",
name: "replace_just_region",
path: "v",
query: url.Values{"c": []string{"12345678"}, "bounce": []string{"123"}},
region: "US-AA",
exp: "ens://v?c=12345678&r=US-AA",
query: url.Values{"c": []string{"12345678"}, "r": []string{"DE"}},
region: "US-BB",
exp: "ens://v?c=12345678&r=US-BB",
},
}

Expand All @@ -97,6 +97,86 @@ func TestBuildEnsURL(t *testing.T) {
}
}

func TestBuildIntentURL(t *testing.T) {
t.Parallel()

expectedSuffix := "#Intent" +
";scheme=ens" +
";package=gov.moosylvania.app" +
";action=android.intent.action.VIEW" +
";category=android.intent.category.BROWSABLE" +
";S.browser_fallback_url=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dgov.moosylvania.app" +
";end"
cases := []struct {
name string
path string
query url.Values
region string
exp string
}{
{
name: "leading_slash_region",
path: "v",
region: "/US-AA",
exp: "intent://v?r=%2FUS-AA" + expectedSuffix,
},
{
name: "trailing_slash_region",
path: "v",
region: "US-AA/",
exp: "intent://v?r=US-AA%2F" + expectedSuffix,
},
{
name: "leading_slash_path",
path: "/v",
region: "US-AA",
exp: "intent://v?r=US-AA" + expectedSuffix,
},
{
name: "trailing_slash_path",
path: "v/",
region: "US-AA",
exp: "intent://v/?r=US-AA" + expectedSuffix,
},
{
name: "includes_code",
path: "v",
query: url.Values{"c": []string{"1234567890abcdef"}},
region: "US-AA",
exp: "intent://v?c=1234567890abcdef&r=US-AA" + expectedSuffix,
},
{
name: "includes_other",
path: "v",
query: url.Values{"foo": []string{"bar"}},
region: "US-AA",
exp: "intent://v?foo=bar&r=US-AA" + expectedSuffix,
},
{
name: "replace_region",
path: "v",
query: url.Values{"r": []string{"US-XX"}},
region: "US-AA",
exp: "intent://v?r=US-AA" + expectedSuffix,
},
}

for _, tc := range cases {
tc := tc

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

appID := "gov.moosylvania.app"
fallback := "https://play.google.com/store/apps/details?id=gov.moosylvania.app"
got, want := buildIntentURL(tc.path, tc.query, tc.region, appID, fallback), tc.exp
if got != want {
t.Errorf("expected %q to be %q", got, want)
}
})
}
}

func TestAgentDetection(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -168,17 +248,28 @@ func TestAgentDetection(t *testing.T) {
func TestDecideRedirect(t *testing.T) {
t.Parallel()

expectedSuffix := "#Intent" +
";scheme=ens" +
";package=gov.moosylvania.app" +
";action=android.intent.action.VIEW" +
";category=android.intent.category.BROWSABLE" +
";S.browser_fallback_url=https%3A%2F%2Fandroid.example.com%2Fstore%2Fmoosylvania" +
";end"

appLinkBoth := AppStoreData{
AndroidURL: "https://android.example.com/store/moosylvania",
IOSURL: "https://ios.example.com/store/moosylvania",
AndroidURL: "https://android.example.com/store/moosylvania",
AndroidAppID: "gov.moosylvania.app",
IOSURL: "https://ios.example.com/store/moosylvania",
}
appLinkOnlyAndroid := AppStoreData{
AndroidURL: "https://android.example.com/store/moosylvania",
IOSURL: "",
AndroidURL: "https://android.example.com/store/moosylvania",
AndroidAppID: "gov.moosylvania.app",
IOSURL: "",
}
appLinkNeither := AppStoreData{
AndroidURL: "",
IOSURL: "",
AndroidURL: "",
AndroidAppID: "",
IOSURL: "",
}

userAgentAndroid := "Android"
Expand All @@ -202,38 +293,24 @@ func TestDecideRedirect(t *testing.T) {
expected string
}{
{
name: "moosylvania_android_pre_bounce",
name: "moosylvania_android_both",
url: "https://moosylvania.gov/v?c=1234567890abcdef",
userAgent: userAgentAndroid,
appStoreData: &appLinkBoth,
expected: "https://moosylvania.gov/v?bounce=1&c=1234567890abcdef",
},
{
name: "moosylvania_android_post_bounce",
url: "https://moosylvania.gov/v?bounce=1&c=1234567890abcdef",
userAgent: userAgentAndroid,
appStoreData: &appLinkBoth,
expected: "https://android.example.com/store/moosylvania",
expected: "intent://v?c=1234567890abcdef&r=US-MOO" + expectedSuffix,
},
{
name: "moosylvania_android_pre_bounce_relative",
name: "moosylvania_android_both_relative",
altURL: &relativePinURL,
userAgent: userAgentAndroid,
appStoreData: &appLinkBoth,
expected: "/v?bounce=1&c=1234567890abcdef",
expected: "intent://v?c=1234567890abcdef&r=US-MOO" + expectedSuffix,
},
{
name: "moosylvania_android_no_applink_pre_bounce",
name: "moosylvania_android_no_applink",
url: "https://moosylvania.gov/v?c=1234567890abcdef",
userAgent: userAgentAndroid,
appStoreData: &appLinkNeither,
expected: "https://moosylvania.gov/v?bounce=1&c=1234567890abcdef",
},
{
name: "moosylvania_android_no_applink_post_bounce",
url: "https://moosylvania.gov/v?c=1234567890abcdef&bounce=1",
userAgent: userAgentAndroid,
appStoreData: &appLinkNeither,
expected: "ens://v?c=1234567890abcdef&r=US-MOO",
},
{
Expand Down