diff --git a/pkg/controller/redirect/appstore.go b/pkg/controller/redirect/appstore.go index 6d0a32643..52e84da02 100644 --- a/pkg/controller/redirect/appstore.go +++ b/pkg/controller/redirect/appstore.go @@ -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. @@ -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 } diff --git a/pkg/controller/redirect/index.go b/pkg/controller/redirect/index.go index cef516e7d..4dbbe6c0a 100644 --- a/pkg/controller/redirect/index.go +++ b/pkg/controller/redirect/index.go @@ -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 @@ -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 +} diff --git a/pkg/controller/redirect/index_test.go b/pkg/controller/redirect/index_test.go index ef0bd7ce7..9edbcdc55 100644 --- a/pkg/controller/redirect/index_test.go +++ b/pkg/controller/redirect/index_test.go @@ -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", }, } @@ -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() @@ -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" @@ -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", }, {